Architecture
Architecture
Section titled “Architecture”This document explains the design and architecture of the dotfiles management system.
Design Philosophy
Section titled “Design Philosophy”The system follows a hybrid architecture combining:
- Go CLI for orchestration, dependency resolution, and state management
- Shell scripts for actual system operations and tool installation
This separation provides:
- Type safety and structure from Go
- Flexibility and portability from shell scripts
- Cross-platform compatibility without requiring platform-specific Go code
For a deeper discussion of why Go was chosen over pure shell or other languages, and why the system builds from source rather than distributing pre-built binaries, see Design Rationale.
System Overview
Section titled “System Overview”Core Components
Section titled “Core Components”1. CLI Layer (Cobra Framework)
Section titled “1. CLI Layer (Cobra Framework)”Location: cmd/dotfiles/
The command-line interface built with Cobra.
Commands:
install- Install modules with dependency resolutionuninstall- Uninstall modules and rollback changesstatus- Show installed modules and their statelist- Show available modulesnew- Generate new module skeletonget-secret- Retrieve secrets (internal, called by shell scripts)render-template- Render Go templates (internal, called by shell scripts)
Entry Point: main.go → cmd/dotfiles/root.go
2. Config Package
Section titled “2. Config Package”Location: internal/config/
Handles configuration loading and management.
Responsibilities:
- Parse
config.yml(main configuration file) - Parse
profiles/*.yml(profile definitions) - Apply environment variable overrides
- Provide module-specific settings lookup
Key Types:
type Config struct { Profile string Secrets SecretsConfig User UserConfig Modules map[string]map[string]interface{}}3. Sysinfo Package
Section titled “3. Sysinfo Package”Location: internal/sysinfo/
Detects system information.
Capabilities:
- Operating system detection (parses
/etc/os-releasefor Linux) - Architecture detection (amd64, arm64)
- Package manager detection (brew, apt, pacman)
- Sudo availability check (non-blocking, 2s timeout)
- Interactive terminal detection
- Dotfiles directory resolution
Key Type:
type SystemInfo struct { OS string Arch string PackageManager string HasSudo bool IsInteractive bool Home string DotfilesDir string}4. Module Package
Section titled “4. Module Package”Location: internal/module/
Core module system implementation.
Components:
Discovery (discovery.go)
Section titled “Discovery (discovery.go)”- Scans the engine’s
modules/directory — and, whenDOTFILES_CONTENT_DIRis set, the overlay’smodules/as a later root - Parses
module.ymlfiles - Validates module schema
- Sorts by priority, then name
- Content-wins layering: modules are merged across roots keyed on
name(not directory). A module defined only in the engine is taggedbuilt-in; a later (content) root that shadows a same-name engine module isoverride(whole-module replacement); a module that exists only in a content root iscustom.dotfiles list/statussurface these tags in a Source column when an overlay contributes.
Config overlay (config.Load)
Section titled “Config overlay (config.Load)”- Loads the base
config.yml, then deep-merges the overlay’sconfig.yml($DOTFILES_CONTENT_DIR/config.yml) over it — only the keys the overlay sets are changed, and YAML types are preserved. - Profiles resolve overlay-first: a bare
--profile nameprefers the overlay’sprofiles/name.ymlover the engine’s, so a content profile overrides a same-name built-in. - With no content dir set, every layer collapses to the engine alone — behavior is identical to having no overlay.
Schema (schema.go)
Section titled “Schema (schema.go)”- Defines module structure
- YAML unmarshaling
- Validation rules
type Module struct { Name string Description string Version string Priority int Dependencies []string OS []string Requires []string Files []FileEntry Prompts []Prompt Tags []string Dir string}Resolver (resolver.go)
Section titled “Resolver (resolver.go)”- Dependency resolution using Kahn’s algorithm (topological sort)
- Transitive dependency expansion (BFS)
- OS compatibility filtering
- Cycle detection with descriptive errors
- Deterministic ordering (priority → name)
Algorithm:
- Build dependency graph
- Expand transitive dependencies
- Filter incompatible modules (OS mismatch)
- Topological sort with priority ordering
- Detect and report cycles
Runner (runner.go)
Section titled “Runner (runner.go)”- Module execution orchestration
- Operation recording for rollback capability
- 8-phase lifecycle per module:
- Prompts (if interactive)
- Environment variable setup
- Template context preparation
- OS-specific script execution
- Install script execution (with operation tracking)
- File deployment (symlink/copy/template, with operation tracking)
- Verification script execution
- State recording (including operation history)
- Automatic rollback on failure with interactive prompt
Interface Pattern:
type RunnerUI interface { StartSpinner(msg string) any StopSpinner(spinner any, success bool, msg string) // ... other UI methods}This interface decouples the module package from the UI package, preventing import cycles.
5. Secrets Package
Section titled “5. Secrets Package”Location: internal/secrets/
Pluggable secrets provider system.
Interface:
type Provider interface { Name() string Available() bool Authenticate() error IsAuthenticated() bool GetSecret(ref string) (string, error)}Implementations:
- 1Password (
onepassword.go) - Integrates withopCLI - Noop (
noop.go) - No-op provider for systems without secrets. This is the shipped default (secrets.provider: noopin the committedconfig.yml), so installs need no external tooling; 1Password is opt-in via the overlay orDOTFILES_SECRETS_PROVIDER.
1Password Provider:
- Checks
opCLI availability - Authenticates via
op vault list - Retrieves secrets:
op read op://vault/item/field - 30-second timeout per operation
- Caches authentication status
6. Template Package
Section titled “6. Template Package”Location: internal/template/
Go template rendering with custom functions.
Context:
type Context struct { User map[string]string // lowercase keys: name, email, github_user OS string // Operating system Arch string // Architecture Home string // Home directory DotfilesDir string // Repository path XDGConfigHome string // Resolved XDG_CONFIG_HOME (env or ~/.config) Module map[string]any // config.yml modules.<name>.* settings only (NOT prompt answers) Secrets map[string]string // always an empty map here; secrets reach scripts via get_secret Env map[string]string // env overrides, incl. DOTFILES_PROMPT_* (prompt answers)}Access .User with lowercase keys — {{ .User.name }}, {{ .User.email }},
{{ .User.github_user }} (.User.Name renders empty). .Module carries only the
module’s config.yml settings (including any content-overlay values, type-preserving);
prompt answers are not in .Module — they arrive via .Env as DOTFILES_PROMPT_*
(e.g. index .Env "DOTFILES_PROMPT_SSH_KEY_TYPE"). Since phase 4C the shell-invoked
render-template subcommand builds this same context (with overlay) as the in-process
runner.
Custom Functions:
env- Get environment variabledefault- First non-empty valueupper,lower- Case conversioncontains- Substring checkjoin- Join slice with separatortrimSpace- Whitespace trimming
7. State Package
Section titled “7. State Package”Location: internal/state/
Persistent module state tracking with operation history for rollback.
Storage:
- Location:
~/.dotfiles/.state/ - Format: JSON per module
- Filename:
<module-name>.json
ModuleState:
type ModuleState struct { Name string Version string Status string // "installed", "failed", "removed" InstalledAt time.Time UpdatedAt time.Time OS string Error string Checksum string Operations []Operation // Operation history for rollback}Operation:
type Operation struct { Type string // "file_deploy", "dir_create", "script_run", "package_install" Action string // "created", "modified", "backed_up", "symlinked", "executed" Path string // File path or package name Timestamp time.Time Metadata map[string]string // Additional context (backup_path, source, type, etc.)}Rollback Capability:
CanRollback()- Check if operations can be reversedRollbackInstructions()- Generate human-readable rollback planRecordOperation()- Add operation to history with timestamp
Operation Types:
- file_deploy: File or symlink creation/modification
- dir_create: Directory creation
- script_run: Shell script execution (informational, not rolled back)
- package_install: Package manager installation (informational, not rolled back)
8. UI Package
Section titled “8. UI Package”Location: internal/ui/
Terminal user interface with colors and spinners.
Features:
- ANSI color output (Catppuccin Mocha palette)
- Animated spinners (braille characters)
- Interactive prompts (input, confirm, choice)
- Execution plan visualization
- TTY detection with graceful fallback
Colors:
const ( ColorRed = "\033[38;5;210m" ColorGreen = "\033[38;5;166m" ColorYellow = "\033[38;5;229m" ColorBlue = "\033[38;5;147m" ColorGray = "\033[38;5;245m" ColorReset = "\033[0m")9. Logging Package
Section titled “9. Logging Package”Location: internal/logging/
Structured logging built on Go’s log/slog (Go 1.21+).
Logger Interface:
type Logger interface { Info(msg string, args ...any) Warn(msg string, args ...any) Error(msg string, args ...any) Debug(msg string, args ...any) Success(msg string, args ...any) With(args ...any) Logger WithGroup(name string) Logger}Handlers:
- PrettyHandler: Colorized human-readable output for terminals
[INFO],[WARN],[ERROR],[DEBUG],[OK]level prefixes- ANSI color coding (cyan, yellow, red, magenta, green)
- Compact key=value attribute formatting
- JSONHandler: Machine-readable structured output
- Standard
log/slogJSON format - Useful for log aggregation and analysis
- Standard
Configuration:
type Config struct { Level string // "debug", "info", "warn", "error" Format string // "pretty", "json" Output io.Writer // Defaults to os.Stderr AddSource bool // Include source file/line}Usage:
# Enable JSON loggingdotfiles install git --log-json
# Enable debug loggingdotfiles install git --verboseData Flow
Section titled “Data Flow”Installation Flow
Section titled “Installation Flow”The installation flow proceeds through system detection, module discovery, dependency resolution, and sequential module execution (prompts → environment setup → OS script → install script → file deployment → verification → state recording).
Module Execution Flow
Section titled “Module Execution Flow”Each module runs through 8 phases: prompts, environment variable injection, template context preparation, OS-specific script, install script, file deployment, verification, and state recording.
Shell Script Execution
Section titled “Shell Script Execution”The Go runner wraps each shell script in a generated wrapper that sources lib/helpers.sh first, then executes the module script. All DOTFILES_* environment variables are available to the script.
Dependency Resolution
Section titled “Dependency Resolution”Algorithm: Kahn’s Topological Sort
Section titled “Algorithm: Kahn’s Topological Sort”Example
Section titled “Example”# Module dependenciesssh: []git: [ssh]zsh: [git]neovim: [git]Execution order:
- ssh (priority 20, no deps)
- git (priority 30, after ssh)
- zsh (priority 40, after git)
- neovim (priority 50, after git)
Note: zsh and neovim could run in parallel since they only depend on git, but the system runs sequentially for simplicity.
Module Lifecycle
Section titled “Module Lifecycle”Module Directory Structure:modules/example/├── module.yml # Metadata├── install.sh # Main logic (required)├── verify.sh # Verification (optional)├── os/ # OS-specific (optional)│ ├── macos.sh│ ├── ubuntu.sh│ └── arch.sh└── files/ # Config files (optional) └── config.confExecution Phases
Section titled “Execution Phases”1. Prompts
- Only in interactive mode
- Answers stored as
DOTFILES_PROMPT_<KEY>
2. Environment Variables
- System info: OS, arch, package manager
- Paths: home, dotfiles dir, module dir
- User config: name, email, github user
- Execution context: dry-run, verbose flags
- Prompt answers
3. Template Context
- Combines all data sources
- Available to template rendering
4. OS Script
- Runs only
os/${DOTFILES_OS}.shif it exists - Platform-specific setup
- Has access to helpers
5. Install Script
- Runs
install.sh(required) - Main installation logic
- Has access to helpers
6. File Deployment
- Processes all
filesfrom module.yml - Creates backups of existing files
- Symlinks, copies, or renders templates
7. Verification
- Runs
verify.shif it exists - Post-install checks
- Can fail the module
8. State Recording
- Writes JSON to
.state/directory - Records success/failure, timestamps, errors
Communication Patterns
Section titled “Communication Patterns”Go → Shell
Section titled “Go → Shell”Environment Variables:
export DOTFILES_OS="ubuntu"export DOTFILES_ARCH="amd64"export DOTFILES_VERBOSE="true"# ... many moreWrapper Script:
#!/usr/bin/env bashset -euo pipefailsource lib/helpers.shsource modules/example/install.shShell → Go
Section titled “Shell → Go”Subprocess Calls:
# In module script:SECRET=$(get_secret "op://vault/item/field")
# Helper function calls Go CLI:get_secret() { "$DOTFILES_BIN" get-secret --ref "$1"}# In module script:render_template "config.tmpl" ~/.config/app/config
# Helper function calls Go CLI:render_template() { "$DOTFILES_BIN" render-template --src "$1" --dest "$2"}Design Patterns
Section titled “Design Patterns”1. Interface Decoupling
Section titled “1. Interface Decoupling”Problem: Module package needed UI functionality but importing UI caused cycles.
Solution: RunnerUI interface in module package:
// In module packagetype RunnerUI interface { StartSpinner(msg string) any StopSpinner(spinner any, success bool, msg string)}
// UI package implements itfunc (u *UI) StartSpinner(msg string) any { // Implementation}2. Pluggable Providers
Section titled “2. Pluggable Providers”Secrets Provider:
type Provider interface { Name() string Available() bool Authenticate() error IsAuthenticated() bool GetSecret(ref string) (string, error)}Easy to add new providers (AWS Secrets Manager, HashiCorp Vault, etc.)
3. Phased Execution
Section titled “3. Phased Execution”Module execution is broken into discrete phases:
- Clear separation of concerns
- Easy to add new phases
- Consistent error handling per phase
4. Composition over Inheritance
Section titled “4. Composition over Inheritance”Modules are composed of:
- Metadata (module.yml)
- Scripts (install.sh, os/*.sh, verify.sh)
- Files (files/*)
No base class or inheritance hierarchy.
Performance Considerations
Section titled “Performance Considerations”Parallel Execution
Section titled “Parallel Execution”Currently sequential, but architecture supports parallel execution:
- Dependency graph enables parallelism
- Modules at same dependency level could run concurrently
- State tracking handles concurrent writes
Caching
Section titled “Caching”- Module Discovery: Cached during execution
- System Info: Detected once at startup
- Authentication: 1Password auth cached for session
- Docker Layer Caching: CI uses GitHub Actions cache
Minimal Dependencies
Section titled “Minimal Dependencies”- Small Go binary (~5-10MB)
- No runtime dependencies except bash
- Modules pull in their own dependencies
Security Model
Section titled “Security Model”Trusted Boundaries
Section titled “Trusted Boundaries”Trusted:
- Environment variables
- CLI flags
- config.yml (user’s own file)
- module.yml files (repo content)
Untrusted:
- Network downloads (modules should verify checksums)
- External secrets (validated by provider)
Secrets Handling
Section titled “Secrets Handling”- Never logged or displayed
- Passed via environment or temp files
- Provider authentication required
- 30-second timeouts prevent hanging
File Operations
Section titled “File Operations”- Automatic backups before overwrite
- Permissions preserved on copy
- Symlinks created safely
- No arbitrary file writes (destination in module.yml)
Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”- Located in
internal/*/with*_test.go - Test individual packages in isolation
- Run with
go test ./...
Integration Tests
Section titled “Integration Tests”- Docker-based for Ubuntu and Arch
- Full installation in clean container
- Verify all modules work end-to-end
- Run with
make test-integration
CI Pipeline
Section titled “CI Pipeline”Unit Tests (always) ↓Integration Tests (matrix: ubuntu, arch) ↓All green → Ready to mergeExtensibility Points
Section titled “Extensibility Points”Add New Command
Section titled “Add New Command”- Create
cmd/dotfiles/newcommand.go - Implement Cobra command
- Add to root command
Add New Module
Section titled “Add New Module”- Create
modules/newmodule/ - Add
module.yml - Add
install.sh - Optionally add OS scripts, files
Add New Secrets Provider
Section titled “Add New Secrets Provider”- Implement
secrets.Providerinterface - Add provider factory in config
- Update config.yml schema
Add Template Function
Section titled “Add Template Function”- Add function to
template/render.go - Register in
template.FuncMap
Future Enhancements
Section titled “Future Enhancements”Potential improvements:
- Parallel Module Execution - Run independent modules concurrently
- Module Marketplace - Discover and install community modules
- Rollback Support - Undo failed installations
- Diff Preview - Show file changes before applying
- Remote Profiles - Load profiles from URLs
- Plugin System - Extend CLI with plugins
- GUI Interface - Web or desktop UI for management