Skip to content

Architecture

How the dotfiles repository is organized and why.

External tools (installed from GitHub, not in this repo):

  • toolbox: Go app via go install github.com/datapointchris/toolbox
  • sesh: Go app via go install github.com/joshmedeski/sesh/v2
  • theme, font: Bash tools cloned to ~/.local/share/

Two-layer approach: common base + platform overlay.

How it works:

  1. Links configs/common/ configs to $HOME
  2. Overlays platform-specific files (auto-detected: macos, wsl, arch, or generic linux)
  3. Links apps from apps/{platform}/ to ~/.local/bin/
  4. Links shell source files from shell/{platform}/ to ~/.local/shell/

Common commands (the dotfiles CLI works from any directory; task is equivalent but only from inside the repo — see Management Interface):

dotfiles link               # Deploy all symlinks
dotfiles relink             # Complete refresh (remove and recreate)
dotfiles update             # Update everything (or a subset: --mine, --no-system)
dotfiles doctor             # Health check: symlinks + package-manifest drift
dotfiles symlinks check     # Verify symlinks are correct
dotfiles symlinks show      # Show all symlinks

Example results:

  • configs/common/.config/zsh/.zshrc~/.config/zsh/.zshrc
  • configs/macos/.gitconfig~/.gitconfig (overrides common)
  • apps/common/menu~/.local/bin/menu

Package Management

System Packages: Homebrew (macOS), apt (Ubuntu/WSL, and generic Debian/Ubuntu LXCs), pacman (Arch)

Language runtimes: managed per language via install/packages.yml — version managers where useful (uv, rustup, go) or system packages otherwise (Node.js)

Why separate: Version managers provide cross-platform consistency and project-specific versions without system conflicts.

Machine Manifests

Installation is driven by machine manifests in install/manifests/. Each manifest defines exactly what gets installed. Every installed tool is declared as a name in a list; the name must resolve to a catalog entry in install/packages.yml. packages verify enforces this bidirectionally (every name → an entry, every entry → a name that references it or a warning).

# install/manifests/archlinux-personal-workstation.yml
machine: archlinux-personal-workstation
platform: archlinux

function_groups: [core, git, python, aws, docker, network, reference, node, fzf]
alias_groups: [core]

system_packages: workstation   # or `core` for a minimal server (linux-lxc-server)
go_tools: [task, cheat, terraform-docs, ...]
github_releases: [fzf, neovim, lazygit, yazi, tree-sitter, tenv, ...]
custom_installers: [bats, awscli, claude-code, terraform-ls, ...]
cargo_packages: [bat, fd-find, eza, zoxide, ...]
npm_globals: [typescript-language-server, prettier, ...]
uv_tools: [ruff, mypy, basedpyright, ...]
git_uv_tools: [refcheck, indy, ...]
# ... etc

Runtime installation is derived from list presence, not from explicit booleans. A non-empty go_tools: list triggers the Go runtime install; a non-empty npm_globals: list triggers nvm + Node; uv_tools: or git_uv_tools: triggers uv. The deprecated go: true / rust: true / nvm: true / uv: true / tenv: true gates were removed in the Phase 1.6 cleanup — packages verify flags any manifest that still sets them.

Run installation with: bash install.sh --machine archlinux-personal-workstation

Shell Source Files

Shell functions and aliases live in shell/ organized by platform, deployed via symlinks — no build step required.

  • Cross-platform: shell/common/functions.sh and shell/common/aliases.sh~/.local/shell/
  • Platform-specific: shell/{platform}/{platform}.sh (macos, arch, wsl, linux) → ~/.local/shell/{platform}.sh
  • .zshrc sources them explicitly using the $PLATFORM env var: source "$SHELL_DIR/$PLATFORM.sh"

Windows Git Bash is the only exception — it needs a concatenated combined.sh for startup performance, generated by install/wsl/sync-windows-shell.sh.

Platform Detection

Shell (configs/common/.config/zsh/.zshrc):

if [[ "$OSTYPE" == "darwin"* ]]; then
    # macOS
elif [[ -f /proc/version ]] && grep -q Microsoft /proc/version; then
    # WSL
elif [[ -f /etc/arch-release ]]; then
    # Arch
else
    # generic Debian/Ubuntu Linux → the `linux` platform (LXCs, small boxes)
fi

Install script: Platform is read from the machine manifest via manifest_field "platform" rather than auto-detected.

Configuration Layers

Configurations use inheritance: shared base with platform overrides.

Example: Git Config

macOS (configs/macos/.gitconfig):

[core]
    editor = code --wait
[credential]
    helper = osxkeychain

WSL (configs/wsl/.gitconfig):

[core]
    editor = nvim
[credential]
    helper = /mnt/c/Program\\ Files/Git/mingw64/bin/git-credential-wincred.exe

Example: Neovim

Common (configs/common/.config/nvim/): Base LSP, core plugins, keybindings

Platform-specific (optional): platform LSP configs

Design Decisions

Symlinks over Stow: Custom tool provides better two-layer linking, clearer error messages, platform awareness.

Taskfile over Makefile: Cross-platform consistency, better syntax for complex commands, modular includes, self-documenting.

Version Managers for Languages: Same Node/Python versions across platforms, project-specific versions, no system conflicts.

Unified Theme System: The theme CLI generates consistent configs for ghostty, tmux, btop, and Neovim from a single theme.yml source file per theme.

Advantages

Minimal Duplication: Only platform differences exist in platform directories.

Clear Separation: configs/common/ for shared, platform dirs for quirks only, apps/ for tools, install/ for repo tooling.

Easy Maintenance: Update shared config once, all platforms benefit.

Testable: Each platform can be tested independently with Docker containers.

Trade-offs

Symlink Complexity: Two-layer system adds complexity, but symlinks tool handles it with clear errors.

Platform Knowledge: Need to know whether to edit configs/common/ or platform dir. Experience makes this clear.

See Platform Differences for platform-specific quirks.

Deep Dives