Anvil

Declarative Workstation Configuration Management

Anvil is a configuration management tool that lets you define your development environment in YAML. Describe the packages, configuration files, scripts, and environment variables your workstation needs, and Anvil takes care of the rest — installing software, deploying dotfiles, running setup scripts, and validating that everything is healthy.

Key Features

  • Package management — install software via winget with version pinning (Homebrew and APT planned)
  • File synchronization — deploy configuration files with automatic backup and integrity checks
  • Script execution — run PowerShell setup and validation scripts with timeout and elevation support
  • Workload inheritance — compose configurations with DRY principles using extends
  • Health checks — validate that the live system matches your workload definition
  • Multiple output formats — table, JSON, YAML, and HTML reports
  • Backup and restore — snapshot and roll back system state
  • Shell completions — tab completion for PowerShell, Bash, Zsh, and Fish

Quick Install

# From crates.io (requires Rust 1.75+)
cargo install anvil-cli

Or download a pre-built binary from the Releases page.

Quick Start

# List available workloads
anvil list

# Preview what an install would do
anvil install rust-developer --dry-run

# Install a workload
anvil install rust-developer

# Verify system health
anvil health rust-developer

Learn More

User Guide

A comprehensive guide to using Anvil for workstation configuration management.

1. Introduction

What is Anvil?

Anvil is a declarative configuration management tool for developer workstations. It allows you to define your development environment in YAML files and automatically:

  • Install software packages via package managers (currently winget; Homebrew and APT planned)
  • Copy and manage configuration files
  • Execute setup and validation scripts
  • Verify system health against your defined configuration

Key Concepts

  • Workload: A configuration bundle containing package definitions, files to deploy, and scripts to run
  • Package: A software application to be installed via winget
  • File: A configuration file to be copied to your system
  • Script: A PowerShell or CMD script to execute during installation or health checks
  • Inheritance: The ability to compose workloads by extending other workloads

System Requirements

Current platform: Windows

  • Windows 10 (version 1809 or later) or Windows 11
  • Windows Package Manager (winget) version 1.4 or later
  • PowerShell 5.1 or later (included with Windows)
  • Administrator access (for some operations)

Cross-platform support (macOS, Linux) is on the roadmap.


2. Installation

Install from crates.io

# Prerequisites: Rust 1.75+
cargo install anvil-cli

Download Pre-built Binary

  1. Download the latest release from the Releases page

  2. Extract the archive:

    Expand-Archive anvil-v0.3.1-windows-x64.zip -DestinationPath C:\Tools\anvil
    
  3. Add to your PATH:

    # Add to current session
    $env:PATH += ";C:\Tools\anvil"
    
    # Add permanently (User scope)
    [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\Tools\anvil", "User")
    

Build from Source

# Prerequisites: Rust 1.75+ and Visual Studio Build Tools

# Clone the repository
git clone https://github.com/kafkade/anvil.git
cd anvil

# Build release binary
cargo build --release

# The binary is at target/release/anvil.exe

Verify Installation

anvil --version
# Output: anvil 0.3.1

anvil --help
# Shows available commands and options

Shell Completions Setup

Generate and install shell completions for better command-line experience:

PowerShell

# Generate completions
anvil completions powershell > $HOME\Documents\WindowsPowerShell\anvil.ps1

# Add to your PowerShell profile
Add-Content $PROFILE '. $HOME\Documents\WindowsPowerShell\anvil.ps1'

Bash (WSL/Git Bash)

# Generate completions
anvil completions bash > ~/.local/share/bash-completion/completions/anvil

# Or add to .bashrc
anvil completions bash >> ~/.bashrc

Zsh

# Generate completions
anvil completions zsh > ~/.zfunc/_anvil

# Add to .zshrc (before compinit)
fpath+=~/.zfunc

3. Quick Start

List Available Workloads

See what workloads are available:

anvil list

Output:

Available Workloads:
  essentials         Core development tools and productivity utilities
  rust-developer     Rust development environment (extends essentials)
  python-developer   Python development environment (extends essentials)

View Workload Details

Inspect what a workload will do:

anvil show rust-developer

Dry Run Installation

Preview what would happen without making changes:

anvil install rust-developer --dry-run

Install a Workload

Apply a workload configuration:

anvil install rust-developer

Check System Health

Verify your system matches the workload definition:

anvil health rust-developer

4. Command Reference

install

Apply a workload configuration to your system.

Synopsis:

anvil install <WORKLOAD> [OPTIONS]

Arguments:

  • <WORKLOAD> - Name of the workload to install

Options:

OptionDescription
--dry-runPreview actions without making changes
--forceForce reinstallation of packages
--skip-packagesSkip package installation
--skip-filesSkip file operations
--skip-scriptsSkip script execution
--output <FORMAT>Output format: table, json, yaml
--path <DIR>Custom workload search path

Examples:

# Standard installation
anvil install rust-developer

# Preview only
anvil install rust-developer --dry-run

# Skip packages (only copy files and run scripts)
anvil install rust-developer --skip-packages

# Use JSON output for scripting
anvil install rust-developer --dry-run --output json

# Install from custom directory
anvil install my-workload --path C:\Workloads

Exit Codes:

  • 0 - Success
  • 1 - General error
  • 2 - Workload not found
  • 3 - Package installation failed
  • 4 - File operation failed
  • 5 - Script execution failed

health

Validate system state against a workload definition.

Synopsis:

anvil health <WORKLOAD> [OPTIONS]

Arguments:

  • <WORKLOAD> - Name of the workload to check

Options:

OptionDescription
--output <FORMAT>Output format: table, json, yaml, html
--file <PATH>Write output to file
--verboseShow detailed check results
--path <DIR>Custom workload search path

Examples:

# Basic health check
anvil health rust-developer

# Detailed output
anvil health rust-developer --verbose

# Generate JSON report
anvil health rust-developer --output json --file health-report.json

# Generate HTML report
anvil health rust-developer --output html --file report.html

Understanding Health Reports:

Health checks verify:

  • Packages: Are required packages installed? Correct versions?
  • Files: Do configuration files exist with expected content?
  • Scripts: Do health check scripts pass?

Status indicators:

  • ✓ (Green) - Check passed
  • ✗ (Red) - Check failed
  • ! (Yellow) - Warning or partial match

list

List available workloads.

Synopsis:

anvil list [OPTIONS]

Options:

OptionDescription
--allInclude hidden/system workloads
--longShow detailed information
--path <DIR>Custom workload search path
--output <FORMAT>Output format: table, json, yaml

Examples:

# Simple list
anvil list

# Detailed list with versions and descriptions
anvil list --long

# JSON output for scripting
anvil list --output json

# List from custom directory
anvil list --path C:\MyWorkloads

show

Display detailed information about a workload.

Synopsis:

anvil show <WORKLOAD> [OPTIONS]

Arguments:

  • <WORKLOAD> - Name of the workload to display

Options:

OptionDescription
--inheritance-treeShow inheritance hierarchy
--resolvedShow fully resolved workload (after inheritance)
--output <FORMAT>Output format: table, json, yaml
--path <DIR>Custom workload search path

Examples:

# Show workload details
anvil show rust-developer

# Show inheritance tree
anvil show rust-developer --inheritance-tree

# Export as YAML
anvil show rust-developer --output yaml

# Show resolved (merged) workload
anvil show rust-developer --resolved

validate

Validate workload syntax and structure.

Synopsis:

anvil validate <WORKLOAD> [OPTIONS]

Arguments:

  • <WORKLOAD> - Name of the workload to validate

Options:

OptionDescription
--strictEnable strict validation mode
--output <FORMAT>Output format: table, json, yaml
--path <DIR>Custom workload search path

Examples:

# Basic validation
anvil validate my-workload

# Strict mode (treats warnings as errors)
anvil validate my-workload --strict

# Validate all bundled workloads
anvil list --output json | ConvertFrom-Json | ForEach-Object { anvil validate $_.name }

Common Validation Errors:

  • Missing required fields (name, version)
  • Invalid workload name format
  • Circular inheritance dependencies
  • Invalid package IDs
  • Non-existent script paths
  • Invalid file paths

init

Create a new workload from a template.

Synopsis:

anvil init <PATH> [OPTIONS]

Arguments:

  • <PATH> - Directory path for the new workload

Options:

OptionDescription
--template <NAME>Template to use: minimal, full, rust, python
--forceOverwrite existing files
--name <NAME>Workload name (defaults to directory name)

Examples:

# Create minimal workload
anvil init C:\Workloads\my-workload

# Create from template
anvil init C:\Workloads\my-rust-env --template rust

# Overwrite existing
anvil init C:\Workloads\existing --force

Available Templates:

  • minimal - Basic structure with required fields only
  • full - Complete example with all features
  • rust - Rust development environment template
  • python - Python development environment template

status

Show current installation status.

Synopsis:

anvil status [WORKLOAD] [OPTIONS]

Arguments:

  • [WORKLOAD] - Optional workload to check status for

Options:

OptionDescription
--output <FORMAT>Output format: table, json, yaml

Examples:

# Overall status
anvil status

# Status for specific workload
anvil status rust-developer

backup

Manage system state backups.

Synopsis:

anvil backup <SUBCOMMAND>

Subcommands:

backup create

Create a new backup of current system state.

# Create backup before changes
anvil backup create

# Create named backup
anvil backup create --name "before-update"

# Create backup for specific workload
anvil backup create --workload rust-developer

backup list

List available backups.

anvil backup list
anvil backup list --output json

backup show

Show details of a specific backup.

anvil backup show <BACKUP_ID>

backup restore

Restore from a backup.

# Restore from backup
anvil backup restore <BACKUP_ID>

# Preview restore
anvil backup restore <BACKUP_ID> --dry-run

backup delete

Delete a backup.

anvil backup delete <BACKUP_ID>
anvil backup delete <BACKUP_ID> --force

config

Manage Anvil configuration.

Synopsis:

anvil config <SUBCOMMAND>

Subcommands:

config show

Display current configuration.

anvil config show
anvil config show --output json

config set

Set a configuration value.

anvil config set workload_paths "C:\Workloads;D:\MoreWorkloads"
anvil config set default_output json
anvil config set backup.enabled true

config reset

Reset configuration to defaults.

anvil config reset
anvil config reset --key workload_paths

config edit

Open configuration file in editor.

anvil config edit

completions

Generate shell completion scripts.

Synopsis:

anvil completions <SHELL>

Arguments:

  • <SHELL> - Target shell: powershell, bash, zsh, fish

Examples:

# Generate PowerShell completions
anvil completions powershell

# Generate and install bash completions
anvil completions bash > /etc/bash_completion.d/anvil

Global Options

These options work with all commands:

OptionShortDescription
--verbose-vIncrease verbosity (use multiple times: -v, -vv, -vvv)
--quiet-qSuppress non-essential output
--no-colorDisable colored output
--help-hShow help information
--version-VShow version information

Examples:

# Verbose output
anvil -v install rust-developer

# Very verbose (debug level)
anvil -vvv health rust-developer

# Quiet mode for scripting
anvil -q install rust-developer

# No colors (for log files)
anvil --no-color list > workloads.txt

5. Configuration

Configuration File Location

Anvil stores its configuration at:

%APPDATA%\anvil\config.toml

Or if ANVIL_CONFIG is set:

$env:ANVIL_CONFIG

Configuration Options

# Global Anvil Configuration

# Default output format (table, json, yaml)
default_output = "table"

# Workload search paths (semicolon-separated)
workload_paths = "C:\\Workloads;D:\\MyWorkloads"

# Enable colored output
color = true

# Default verbosity level (0-3)
verbosity = 0

[backup]
# Enable automatic backups before changes
enabled = true

# Backup directory
path = "%APPDATA%\\anvil\\backups"

# Maximum number of backups to keep
max_count = 10

[packages]
# Default package source
default_source = "winget"

# Allow prerelease versions
allow_prerelease = false

[scripts]
# Default script timeout (seconds)
default_timeout = 300

# Shell for script execution
default_shell = "powershell"

View Current Configuration

anvil config show

Modify Configuration

# Set a value
anvil config set default_output json

# Reset to default
anvil config reset

6. Configuring Workload Search Paths

Anvil searches for workloads in multiple directories. You can add custom paths to include your own workloads alongside the built-in ones.

Adding a Search Path

anvil config set workloads.paths '["~/my-workloads", "/shared/team-workloads"]'

Or edit ~/.anvil/config.yaml directly:

workloads:
  paths:
    - "~/my-workloads"
    - "/shared/team-workloads"

Search Order

Anvil resolves workloads in this priority order:

  1. Explicit path — passed via --path flag
  2. User-configured — paths from ~/.anvil/config.yaml
  3. Default locations — bundled workloads, local data directory, current directory

When the same workload name exists in multiple paths, the first match wins. Use anvil list --all-paths to see all discovered paths including shadowed duplicates.

Complete Config Example

# Anvil global configuration
# Location: ~/.anvil/config.yaml

workloads:
  paths:
    - "~/my-workloads"           # Personal workloads
    - "~/work/team-workloads"    # Team-shared workloads

logging:
  level: info

7. Working with Workloads

Discovering Workloads

Anvil searches for workloads in these locations (in priority order):

  1. Path specified with --path option
  2. User-configured paths (from ~/.anvil/config.yaml)
  3. Default locations (bundled workloads, local data directory, current directory)
# List all available workloads
anvil list

# List with details
anvil list --long

# List from specific directory
anvil list --path C:\MyWorkloads

Understanding Workload Inheritance

Workloads can extend other workloads to inherit their configuration:

name: my-rust-env
version: "1.0.0"
extends:
  - essentials        # Inherits packages, files, scripts
  - rust-developer    # Adds Rust-specific config

View the inheritance tree:

anvil show my-rust-env --inheritance-tree

Output:

my-rust-env
├── essentials
└── rust-developer
    └── essentials

Using Custom Workload Directories

# One-time use
anvil list --path C:\MyWorkloads
anvil install my-workload --path C:\MyWorkloads

# Configure permanently
anvil config set workload_paths "C:\MyWorkloads"

Validating Before Install

Always validate workloads before installation:

# Validate syntax
anvil validate my-workload

# Strict validation
anvil validate my-workload --strict

# Preview installation
anvil install my-workload --dry-run

8. Output Formats

Anvil supports multiple output formats for different use cases.

Table (Default)

Human-readable format for terminal display:

anvil list
┌──────────────────┬─────────┬────────────────────────────────────────────────────┐
│ Name             │ Version │ Description                                        │
├──────────────────┼─────────┼────────────────────────────────────────────────────┤
│ essentials       │ 2.0.0   │ Core development tools and productivity utilities  │
│ rust-developer   │ 1.0.0   │ Rust development environment                       │
└──────────────────┴─────────┴────────────────────────────────────────────────────┘

JSON

Machine-readable format for scripting and automation:

anvil list --output json
[
  {
    "name": "essentials",
    "version": "2.0.0",
    "description": "Core development tools and productivity utilities"
  },
  {
    "name": "rust-developer",
    "version": "1.0.0",
    "description": "Rust development environment"
  }
]

YAML

Configuration-friendly format:

anvil show rust-developer --output yaml
name: rust-developer
version: "1.0.0"
description: Rust development environment
extends:
  - essentials
packages:
  winget:
    - id: Rustlang.Rustup

HTML

Rich reports for documentation:

anvil health rust-developer --output html --file report.html

Generates a styled HTML document with:

  • Summary statistics
  • Detailed check results
  • Pass/fail indicators
  • Timestamp and system info

9. Environment Variables

VariableDescriptionDefault
ANVIL_CONFIGConfiguration file path%APPDATA%\anvil\config.toml
ANVIL_WORKLOADSAdditional workload search paths(none)
ANVIL_LOGLog level: error, warn, info, debug, tracewarn
NO_COLORDisable colored output (any value)(unset)
ANVIL_BACKUP_DIRBackup storage directory%APPDATA%\anvil\backups

Examples:

# Use custom config file
$env:ANVIL_CONFIG = "C:\config\anvil.toml"
anvil list

# Add workload search paths
$env:ANVIL_WORKLOADS = "C:\Workloads;D:\MoreWorkloads"
anvil list

# Enable debug logging
$env:ANVIL_LOG = "debug"
anvil install rust-developer

# Disable colors
$env:NO_COLOR = "1"
anvil list

10. Best Practices

Always Dry-Run First

Before applying any workload, preview the changes:

anvil install my-workload --dry-run

This shows what will happen without making changes.

Use Health Checks Regularly

Verify your system state periodically:

# Quick check
anvil health rust-developer

# Detailed report
anvil health rust-developer --verbose --output html --file health.html

Keep Backups

Enable automatic backups in configuration:

anvil config set backup.enabled true

Or create manual backups before major changes:

anvil backup create --name "before-upgrade"

Version Your Workloads

Store your workloads in version control:

my-workloads/
├── .git/
├── team-base/
│   └── workload.yaml
├── frontend-dev/
│   └── workload.yaml
└── backend-dev/
    └── workload.yaml

Use Inheritance Wisely

Create a base workload with common tools:

# team-base/workload.yaml
name: team-base
version: "1.0.0"
packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode

Then extend it for specific roles:

# frontend-dev/workload.yaml
name: frontend-dev
extends:
  - team-base
packages:
  winget:
    - id: OpenJS.NodeJS

Validate Before Committing

Add validation to your CI/CD pipeline:

# Validate all workloads
Get-ChildItem -Directory | ForEach-Object {
    anvil validate $_.Name --strict
}

Use Verbose Output for Debugging

When things go wrong:

# Increase verbosity
anvil -vvv install my-workload

# Enable trace logging
$env:ANVIL_LOG = "trace"
anvil install my-workload

Script Error Handling

In your workload scripts, handle errors gracefully:

# health-check.ps1
try {
    $rustVersion = rustc --version
    if ($LASTEXITCODE -ne 0) {
        Write-Error "Rust not installed"
        exit 1
    }
    Write-Host "Rust installed: $rustVersion"
    exit 0
}
catch {
    Write-Error "Health check failed: $_"
    exit 1
}

Getting Help

Built-in Help

# General help
anvil --help

# Command-specific help
anvil install --help
anvil health --help

Resources

Reporting Issues

When reporting issues, include:

  1. Anvil version: anvil --version
  2. Windows version: winver
  3. Command that failed
  4. Verbose output: anvil -vvv <command>
  5. Relevant workload files (sanitized)

This guide is for Anvil v0.3.1. For other versions, check the corresponding documentation.

Workload Authoring

A comprehensive guide to creating custom workloads for Anvil.

1. Workload Structure

A workload is a directory containing configuration files, scripts, and assets that define a system configuration.

Directory Structure

workload-name/
├── workload.yaml       # Required: workload definition
├── files/              # Optional: files to deploy
│   ├── .config/
│   │   └── app.conf
│   └── settings.json
└── scripts/            # Optional: installation/health scripts
    ├── pre-install.ps1
    ├── post-install.ps1
    └── health-check.ps1

Required Files

  • workload.yaml: The main workload definition file (required)

Optional Directories

  • files/: Contains configuration files to be copied to the target system
  • scripts/: Contains PowerShell or CMD scripts for installation and validation

Naming Conventions

  • Workload directory names should be lowercase with hyphens: my-workload-name
  • Use descriptive names that indicate the workload's purpose
  • Avoid spaces and special characters

2. Schema Reference

The workload.yaml file defines all aspects of your workload configuration.

Complete Schema

# Required fields
name: string                    # Workload identifier
version: string                 # Semantic version (e.g., "1.0.0")

# Optional fields
description: string             # Human-readable description
extends: string[]               # Parent workloads to inherit from
packages: object                # Package definitions
files: array                    # File deployment definitions
scripts: object                 # Script execution definitions
environment: object             # Environment variable configuration
assertions: array               # Declarative health assertions
health: object                  # Health check configuration

Required Fields

name

Unique identifier for the workload. Must be alphanumeric with hyphens and underscores only.

name: my-workload           # Valid
name: my_workload_v2        # Valid
name: "my workload"         # Invalid (spaces)
name: my.workload           # Invalid (dots)

version

Semantic version string. Recommended to follow SemVer.

version: "1.0.0"
version: "2.1.0-beta"
version: "0.1.0"

Optional Fields

description

Human-readable description displayed in listings.

description: "Development environment for Rust projects with debugging tools"

extends

List of parent workloads to inherit from.

extends:
  - essentials
  - rust-developer

packages

Package installation definitions (see Package Definitions).

files

File deployment definitions (see File Definitions).

scripts

Script execution definitions (see Script Definitions).

environment

Environment variable configuration (see Environment Configuration).


3. Package Definitions

Define software packages to install via Windows Package Manager (winget).

Basic Structure

packages:
  winget:
    - id: Publisher.PackageName
    - id: Another.Package

Full Package Options

packages:
  winget:
    - id: Git.Git
      version: "2.43.0"           # Optional: pin to specific version
      source: winget              # Optional: winget, msstore
      override:                    # Optional: additional winget arguments
        - "--scope"
        - "machine"

Package Fields

FieldRequiredDescription
idYesWinget package identifier
versionNoSpecific version to install
sourceNoPackage source: winget or msstore
overrideNoAdditional arguments passed to winget

Finding Package IDs

Use winget to search for packages:

# Search for packages
winget search vscode

# Get exact ID
winget search --exact "Visual Studio Code"

# Show package details
winget show Microsoft.VisualStudioCode

Common package IDs:

PackageID
Visual Studio CodeMicrosoft.VisualStudioCode
GitGit.Git
Windows TerminalMicrosoft.WindowsTerminal
Node.jsOpenJS.NodeJS
PythonPython.Python.3.12
RustRustlang.Rustup
PowerShellMicrosoft.PowerShell

Version Pinning

Pin specific versions when compatibility matters:

packages:
  winget:
    # Pin exact version
    - id: Python.Python.3.12
      version: "3.12.0"
    
    # Use latest (default)
    - id: Git.Git

Check available versions:

winget show Python.Python.3.12 --versions

Override Arguments

Pass custom arguments to winget:

packages:
  winget:
    - id: Microsoft.VisualStudioCode
      override:
        - "--scope"
        - "machine"           # Install for all users
        - "--override"
        - "/SILENT"

4. File Definitions

Define configuration files to copy to the target system.

Basic Structure

files:
  - source: config.json
    destination: "~/.config/app/config.json"

Full File Options

files:
  - source: relative/path/in/workload/file.conf
    destination: "~/target/path/file.conf"
    backup: true                  # Backup existing file
    mode: "0644"                  # Optional: file permissions
    template: false               # Process as Handlebars template
    create_dirs: true             # Create parent directories

File Fields

FieldRequiredDefaultDescription
sourceYes-Path relative to workload directory
destinationYes-Target path on system
backupNotrueBackup existing files before overwriting
modeNo-File permissions (Unix-style, informational on Windows)
templateNofalseProcess file as Handlebars template
create_dirsNotrueCreate parent directories if missing

Path Variables

Use these variables in destination paths:

VariableDescriptionExample
~User home directoryC:\Users\username
${HOME}User home directoryC:\Users\username
${USERPROFILE}User profile directoryC:\Users\username
${APPDATA}Application dataC:\Users\username\AppData\Roaming
${LOCALAPPDATA}Local app dataC:\Users\username\AppData\Local
${WORKLOAD_DIR}Workload directoryPath to current workload

Examples:

files:
  # Home directory
  - source: .gitconfig
    destination: "~/.gitconfig"
  
  # AppData
  - source: settings.json
    destination: "${APPDATA}/MyApp/settings.json"
  
  # Local AppData
  - source: cache.db
    destination: "${LOCALAPPDATA}/MyApp/cache.db"

Templating

Enable Handlebars templating for dynamic content:

files:
  - source: config.toml.hbs
    destination: "~/.config/app/config.toml"
    template: true

Template file (config.toml.hbs):

# Configuration for {{username}}
# Generated on {{date}}

[user]
name = "{{username}}"
home = "{{home}}"

[paths]
workload = "{{workload_dir}}"

Available template variables:

VariableDescription
{{username}}Current username
{{home}}Home directory path
{{computername}}Computer name
{{workload_dir}}Workload directory
{{workload_name}}Workload name
{{date}}Current date
{{env.VAR_NAME}}Environment variable

Directory Copying

Copy entire directories:

files:
  - source: config/
    destination: "~/.config/myapp/"

5. Script Definitions

Define PowerShell or CMD scripts for installation steps and health checks.

Script Categories

scripts:
  pre_install:     # Run before package installation
    - path: scripts/pre-install.ps1
    
  post_install:    # Run after package installation
    - path: scripts/post-install.ps1
    
  health_check:    # Run during health checks
    - path: scripts/health-check.ps1

Full Script Options

scripts:
  post_install:
    - path: scripts/setup.ps1
      shell: powershell           # powershell, pwsh, cmd
      description: "Configure application settings"
      elevated: false             # Run as administrator
      timeout: 300                # Timeout in seconds
      continue_on_error: false    # Continue if script fails
      env:                        # Additional environment variables
        MY_VAR: "value"

Script Fields

FieldRequiredDefaultDescription
pathYes-Path relative to workload directory
shellNopowershellShell: powershell, pwsh, cmd
descriptionNo-Human-readable description
elevatedNofalseRun with administrator privileges
timeoutNo300Timeout in seconds
continue_on_errorNofalseContinue installation if script fails
envNo-Additional environment variables
nameNo-Display name (for health checks)

Health Check Scripts

Health check scripts verify system state:

scripts:
  health_check:
    - path: scripts/check-rust.ps1
      name: "Rust Toolchain"
      description: "Verify Rust is installed and configured"
      timeout: 30

Script Guidelines

Exit Codes

  • 0 - Success
  • Non-zero - Failure
# Good: explicit exit codes
if (Test-Path $requiredFile) {
    exit 0
} else {
    Write-Error "Required file not found"
    exit 1
}

Output Handling

  • Use Write-Host for informational output
  • Use Write-Error for error messages
  • Use Write-Warning for warnings
Write-Host "Installing components..."
Write-Warning "This may take a while"
Write-Error "Installation failed"

Idempotency

Scripts should be safe to run multiple times:

# Good: check before acting
if (-not (Test-Path "C:\Tools\mytool")) {
    Write-Host "Installing mytool..."
    # Install logic here
} else {
    Write-Host "mytool already installed"
}

Error Handling

Use try-catch for robust error handling:

try {
    # Risky operation
    Invoke-WebRequest -Uri $url -OutFile $path
    exit 0
}
catch {
    Write-Error "Failed to download: $_"
    exit 1
}

Sample Scripts

Pre-Install Check

# scripts/pre-install.ps1
# Check prerequisites before installation

$ErrorActionPreference = "Stop"

# Check Windows version
$version = [Environment]::OSVersion.Version
if ($version.Major -lt 10) {
    Write-Error "Windows 10 or later required"
    exit 1
}

# Check available disk space (need 1GB)
$drive = Get-PSDrive C
$freeGB = [math]::Round($drive.Free / 1GB, 2)
if ($freeGB -lt 1) {
    Write-Error "Insufficient disk space. Need 1GB, have ${freeGB}GB"
    exit 1
}

Write-Host "Prerequisites check passed"
exit 0

Post-Install Configuration

# scripts/post-install.ps1
# Configure application after installation

$ErrorActionPreference = "Stop"

try {
    # Install Rust components
    Write-Host "Installing Rust components..."
    rustup component add rustfmt
    rustup component add clippy
    
    # Install common cargo tools
    Write-Host "Installing cargo tools..."
    cargo install cargo-watch
    cargo install cargo-edit
    
    Write-Host "Post-install configuration complete"
    exit 0
}
catch {
    Write-Error "Post-install failed: $_"
    exit 1
}

Health Check

# scripts/health-check.ps1
# Verify Rust development environment

$ErrorActionPreference = "Stop"
$errors = @()

# Check rustc
try {
    $rustVersion = rustc --version
    Write-Host "✓ Rust compiler: $rustVersion"
}
catch {
    $errors += "rustc not found"
}

# Check cargo
try {
    $cargoVersion = cargo --version
    Write-Host "✓ Cargo: $cargoVersion"
}
catch {
    $errors += "cargo not found"
}

# Check rustfmt
try {
    $null = rustfmt --version
    Write-Host "✓ rustfmt installed"
}
catch {
    $errors += "rustfmt not installed"
}

# Report results
if ($errors.Count -gt 0) {
    Write-Error "Health check failed:"
    $errors | ForEach-Object { Write-Error "  - $_" }
    exit 1
}

Write-Host "All health checks passed"
exit 0

6. Environment Configuration

Configure environment variables and PATH additions.

Basic Structure

environment:
  variables:
    - name: MY_VARIABLE
      value: "my-value"
      scope: user
      
  path_additions:
    - "C:\\Tools\\bin"
    - "~\\.local\\bin"

Environment Variables

environment:
  variables:
    - name: RUST_BACKTRACE
      value: "1"
      scope: user              # user or machine
      
    - name: EDITOR
      value: "code"
      scope: user
FieldRequiredDefaultDescription
nameYes-Variable name
valueYes-Variable value
scopeNouserScope: user or machine

PATH Additions

Add directories to the system PATH:

environment:
  path_additions:
    - "C:\\Tools\\bin"
    - "~\\.cargo\\bin"
    - "${LOCALAPPDATA}\\Programs\\bin"

PATH additions are appended to the existing PATH for the specified scope.

Scope

ScopeDescriptionRequires Admin
userCurrent user onlyNo
machineAll users on systemYes

7. Assertions

Assertions are declarative health checks defined directly in workload.yaml. They let you validate system state — such as installed commands, existing files, environment variables, and PATH entries — without writing PowerShell scripts.

Use assertions when your checks are simple conditions (command exists, file exists, env var set). Use health check scripts for complex validation that requires multi-step logic or custom output.

Assertion Structure

Each assertion has a name and a check that specifies a condition:

assertions:
  - name: Git is installed
    check:
      type: command_exists
      command: git

Condition Types

command_exists

Checks whether a command is available on PATH.

- name: cargo is available
  check:
    type: command_exists
    command: cargo

file_exists

Checks whether a file exists at the given path. Supports ~ expansion.

- name: Git config exists
  check:
    type: file_exists
    path: "~/.gitconfig"

dir_exists

Checks whether a directory exists at the given path. Supports ~ expansion.

- name: Cargo directory exists
  check:
    type: dir_exists
    path: "~/.cargo"

env_var

Checks whether an environment variable is set, optionally matching a specific value.

# Check existence only
- name: RUST_BACKTRACE is set
  check:
    type: env_var
    name: RUST_BACKTRACE

# Check existence and value
- name: RUST_BACKTRACE is 1
  check:
    type: env_var
    name: RUST_BACKTRACE
    value: "1"

path_contains

Checks whether the system PATH contains a given substring.

- name: Cargo bin on PATH
  check:
    type: path_contains
    substring: ".cargo/bin"

registry_value

Queries a Windows registry value under HKCU or HKLM. If expected is omitted, the check only asserts the value exists.

- name: Developer mode enabled
  check:
    type: registry_value
    hive: HKLM
    key: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"
    name: AllowDevelopmentWithoutDevLicense
    expected: "1"

shell

Runs an arbitrary shell command; the condition passes when the exit code is 0.

- name: Rust compiler responds
  check:
    type: shell
    command: "rustc --version"
    description: "Rust compiler version check"

Composition with all_of and any_of

Combine conditions with logical operators for complex checks.

all_of — all conditions must pass (AND)

- name: Full Rust toolchain
  check:
    type: all_of
    conditions:
      - type: command_exists
        command: rustc
      - type: command_exists
        command: cargo
      - type: dir_exists
        path: "~/.cargo"

any_of — at least one must pass (OR)

- name: Python is available
  check:
    type: any_of
    conditions:
      - type: command_exists
        command: python
      - type: command_exists
        command: python3

Enabling Assertion Checks

Assertions are evaluated during anvil health when assertion_check is enabled in the health config (it defaults to true):

health:
  package_check: true
  file_check: true
  script_check: true
  assertion_check: true   # Evaluate declarative assertions

Complete Example

name: rust-developer
version: "1.0.0"
description: "Rust development environment"

packages:
  winget:
    - id: Rustlang.Rustup

assertions:
  - name: cargo command exists
    check:
      type: command_exists
      command: cargo
  - name: rustc command exists
    check:
      type: command_exists
      command: rustc
  - name: Cargo directory exists
    check:
      type: dir_exists
      path: "~/.cargo"
  - name: Cargo bin on PATH
    check:
      type: path_contains
      substring: ".cargo/bin"
  - name: RUST_BACKTRACE is set
    check:
      type: env_var
      name: RUST_BACKTRACE
      value: "1"

health:
  package_check: true
  assertion_check: true

8. Inheritance

Workloads can extend other workloads to inherit their configuration.

Basic Inheritance

name: my-rust-dev
version: "1.0.0"

extends:
  - essentials      # Inherit base development tools

Multiple Inheritance

name: full-stack-dev
version: "1.0.0"

extends:
  - essentials
  - rust-developer
  - python-developer

Merge Behavior

When a workload extends parents, configuration is merged:

SectionMerge Behavior
packagesMerged; child packages added to parent packages
filesMerged; child files override parent files with same destination
scriptsMerged; child scripts run after parent scripts
environmentMerged; child variables override parent variables

Override Example

Parent (base/workload.yaml):

name: base
version: "1.0.0"

packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode

files:
  - source: .gitconfig
    destination: "~/.gitconfig"

Child (extended/workload.yaml):

name: extended
version: "1.0.0"

extends:
  - base

packages:
  winget:
    # Adds to parent packages
    - id: Rustlang.Rustup

files:
  # Overrides parent's .gitconfig
  - source: .gitconfig
    destination: "~/.gitconfig"

Inheritance Chains

Anvil resolves inheritance chains automatically:

my-workload
└── rust-developer
    └── essentials

Circular dependencies are detected and rejected:

# workload-a extends workload-b
# workload-b extends workload-a
# ERROR: Circular dependency detected

View Inheritance

# Show inheritance tree
anvil show my-workload --inheritance-tree

# Show fully resolved workload
anvil show my-workload --resolved

9. Variable Expansion

Use variables in paths and values for dynamic configuration.

Supported Variables

VariableDescriptionExample Value
~User home directoryC:\Users\username
${HOME}User home directoryC:\Users\username
${USERNAME}Current usernameusername
${COMPUTERNAME}Machine nameWORKSTATION-01
${WORKLOAD_DIR}Workload directoryC:\Workloads\my-workload
${USERPROFILE}User profile pathC:\Users\username
${APPDATA}Roaming AppDataC:\Users\username\AppData\Roaming
${LOCALAPPDATA}Local AppDataC:\Users\username\AppData\Local
${env:VAR_NAME}Any environment variable(varies)

Usage Examples

files:
  # Home directory shorthand
  - source: .bashrc
    destination: "~/.bashrc"
  
  # Explicit home variable
  - source: config.json
    destination: "${HOME}/.config/myapp/config.json"
  
  # AppData paths
  - source: settings.json
    destination: "${APPDATA}/MyApp/settings.json"
  
  # Environment variable
  - source: custom.conf
    destination: "${env:MY_CUSTOM_PATH}/config.conf"

environment:
  variables:
    - name: MY_APP_HOME
      value: "${HOME}/.myapp"
  
  path_additions:
    - "${HOME}/.local/bin"
    - "${LOCALAPPDATA}/Programs/bin"

10. Best Practices

Workload Design

  1. Use Descriptive Names

    name: rust-developer           # Good
    name: workload1                # Bad
    
  2. Include Version Information

    version: "1.2.0"               # Good: semantic versioning
    version: "latest"              # Bad: not meaningful
    
  3. Write Helpful Descriptions

    description: "Complete Rust development environment with debugging tools and VS Code extensions"
    
  4. Use Inheritance for Common Bases

    # Create a base workload for team-wide tools
    # Then extend it for role-specific setups
    extends:
      - team-base
    

Package Management

  1. Pin Versions When Necessary

    packages:
      winget:
        # Pin when compatibility matters
        - id: Python.Python.3.12
          version: "3.12.0"
        
        # Use latest for frequently updated tools
        - id: Git.Git
    
  2. Use Machine Scope for Shared Tools

    packages:
      winget:
        - id: Microsoft.VisualStudioCode
          override:
            - "--scope"
            - "machine"
    

File Management

  1. Always Enable Backups for Important Files

    files:
      - source: .gitconfig
        destination: "~/.gitconfig"
        backup: true
    
  2. Use Templates for Dynamic Content

    files:
      - source: config.toml.hbs
        destination: "~/.config/app/config.toml"
        template: true
    

Script Safety

  1. Make Scripts Idempotent

    # Check before acting
    if (-not (Test-Path $target)) {
        # Create/install
    }
    
  2. Handle Errors Gracefully

    try {
        # Risky operation
    }
    catch {
        Write-Error "Operation failed: $_"
        exit 1
    }
    
  3. Include Health Checks

    scripts:
      health_check:
        - path: scripts/verify.ps1
          name: "Installation Verification"
    

Testing

  1. Validate Before Committing

    anvil validate my-workload --strict
    
  2. Test with Dry Run

    anvil install my-workload --dry-run
    
  3. Test on Clean System

    • Use a VM or container
    • Document dependencies

11. Example Workloads

Minimal Workload

The simplest valid workload:

# minimal/workload.yaml
name: minimal
version: "1.0.0"
description: "A minimal workload example"

Package-Only Workload

Install software without files or scripts:

# dev-essentials/workload.yaml
name: dev-essentials
version: "1.0.0"
description: "Essential development tools"

packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode
    - id: Microsoft.WindowsTerminal
    - id: JanDeDobbeleer.OhMyPosh

Complete example with all features:

# full-example/workload.yaml
name: full-example
version: "1.0.0"
description: "Complete workload demonstrating all features"

extends:
  - essentials

packages:
  winget:
    - id: Rustlang.Rustup
    - id: LLVM.LLVM
      version: "17.0.6"
    - id: Microsoft.VisualStudio.2022.BuildTools
      override:
        - "--add"
        - "Microsoft.VisualStudio.Workload.VCTools"

files:
  - source: files/.cargo/config.toml
    destination: "~/.cargo/config.toml"
    backup: true
    
  - source: files/vscode/settings.json.hbs
    destination: "${APPDATA}/Code/User/settings.json"
    template: true

scripts:
  pre_install:
    - path: scripts/pre-check.ps1
      description: "Check prerequisites"
      
  post_install:
    - path: scripts/setup-rust.ps1
      description: "Configure Rust toolchain"
      elevated: false
      timeout: 600
      
  health_check:
    - path: scripts/health.ps1
      name: "Rust Environment"
      description: "Verify Rust development environment"

environment:
  variables:
    - name: RUST_BACKTRACE
      value: "1"
      scope: user
      
  path_additions:
    - "~/.cargo/bin"

Inherited Workload

Workload that builds on others:

# rust-advanced/workload.yaml
name: rust-advanced
version: "1.0.0"
description: "Advanced Rust development with WASM and embedded support"

extends:
  - rust-developer

packages:
  winget:
    - id: Docker.DockerDesktop
    - id: WasmEdge.WasmEdge

scripts:
  post_install:
    - path: scripts/setup-targets.ps1
      description: "Add Rust compilation targets"

environment:
  path_additions:
    - "~/.wasmedge/bin"

With corresponding script:

# rust-advanced/scripts/setup-targets.ps1
# Add additional Rust compilation targets

$ErrorActionPreference = "Stop"

try {
    Write-Host "Adding WASM target..."
    rustup target add wasm32-unknown-unknown
    
    Write-Host "Adding embedded targets..."
    rustup target add thumbv7em-none-eabihf
    
    Write-Host "Installing cargo tools for embedded..."
    cargo install cargo-embed
    cargo install probe-run
    
    exit 0
}
catch {
    Write-Error "Setup failed: $_"
    exit 1
}

12. Private Workload Repositories

You can maintain your own workloads in a separate Git repository and configure Anvil to discover them.

my-workloads/
├── my-dev-env/
│   ├── workload.yaml
│   ├── files/
│   │   └── .gitconfig
│   └── scripts/
│       └── setup.ps1
├── team-tools/
│   ├── workload.yaml
│   └── scripts/
│       └── post-install.ps1
└── README.md

Each subdirectory containing a workload.yaml is treated as a separate workload, following the same directory structure as bundled workloads.

Setup

  1. Clone your workloads repository:

    git clone https://github.com/your-org/workloads ~/my-workloads
    
  2. Configure Anvil to search this path:

    anvil config set workloads.paths '["~/my-workloads"]'
    

    Or edit ~/.anvil/config.yaml directly:

    workloads:
      paths:
        - "~/my-workloads"
    
  3. Verify discovery:

    anvil list
    

Tips

  • Inheritance: Private workloads can extends: built-in workloads (e.g., extends: [essentials])
  • Version control: Keep workloads in Git for team sharing and history
  • Multiple repos: Add multiple paths for team-shared and personal workloads
  • Precedence: If your workload has the same name as a built-in one, yours takes priority
  • Validation: Run anvil validate <name> --strict to check your workloads before committing

Complete Config Example

# Anvil global configuration
# Location: ~/.anvil/config.yaml

workloads:
  paths:
    - "~/my-workloads"           # Personal workloads
    - "~/work/team-workloads"    # Team-shared workloads

logging:
  level: info

Resources


This guide is for Anvil v0.3.1. For other versions, check the corresponding documentation.

Troubleshooting

A guide to diagnosing and resolving common Anvil issues.

1. Installation Issues

Anvil won't run

Symptoms:

  • "anvil" is not recognized as a command
  • Application crashes immediately

Solutions:

  1. Check PATH configuration

    Windows:

    where.exe anvil
    # If not found, add to PATH
    $env:PATH += ";C:\path\to\anvil"
    

    Linux/macOS:

    which anvil
    # If not found, add to PATH
    export PATH="$PATH:/path/to/anvil"
    
  2. Verify platform compatibility

    • Anvil currently requires Windows 10 (1809+) or Windows 11
    • Cross-platform support is on the roadmap
  3. Verify the download isn't corrupted

    • Re-download from the releases page
    • Verify the file hash if provided

Package manager not found

Symptoms:

  • Error: "winget is not recognized"
  • Error: "Package manager not available"
  • Package installation fails before starting

Solutions (Windows — winget):

  1. Install Windows Package Manager

    # Open Microsoft Store to App Installer
    Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1"
    
  2. Update App Installer

    • Open Microsoft Store → search "App Installer" → click "Update"
  3. Test availability

    winget --version
    
  4. Restart your terminal after installing winget


2. Package Installation Issues

Package not found

Symptoms:

  • Error: "No package found matching input criteria"
  • Error: "Package 'X' not found in any source"

Solutions:

  1. Verify package ID

    winget search <package-name>
    winget search --exact "Package Name"
    
  2. Check package source in workload

    packages:
      winget:
        - id: SomeApp.App
          source: msstore    # For Microsoft Store apps
    
  3. Update package sources

    winget source update
    

Installation hangs

Symptoms:

  • Installation appears frozen
  • No progress for extended time

Solutions:

  1. Check for interactive prompts

    • Some installers require user interaction
    • Run anvil without --quiet to see prompts
  2. Use silent install overrides

    packages:
      winget:
        - id: SomeApp.App
          override:
            - "--silent"
            - "--accept-license"
    
  3. Check for pending system updates

    • Pending OS updates can block installations
    • Complete any pending updates and restart

Access denied during installation

Symptoms:

  • Error: "Access is denied"
  • Error: "Administrator privileges required"

Solutions:

  1. Run with elevation (Windows)

    Start-Process powershell -Verb RunAs
    anvil install my-workload
    
  2. Use user-scope installation

    packages:
      winget:
        - id: Package.Name
          override:
            - "--scope"
            - "user"
    

3. File Operation Issues

Permission denied

Symptoms:

  • Error: "Permission denied" when copying files
  • File operations fail

Solutions:

  1. Use user-writable paths

    files:
      - source: config.json
        destination: "~/.config/app/config.json"  # Expands to user home
    
  2. Check target directory permissions

    Windows:

    Get-Acl "C:\path\to\directory" | Format-List
    

    Linux/macOS:

    ls -la /path/to/directory
    
  3. Run as administrator for system-level paths


File not found

Symptoms:

  • Error: "Source file not found"

Solutions:

  1. Verify source path — paths are relative to the workload directory

    files:
      - source: files/config.json      # Relative to workload dir
        destination: "~/.config/app/config.json"
    
  2. Check workload directory structure

    my-workload/
    ├── workload.yaml
    └── files/
        └── config.json
    

Backup failed

Symptoms:

  • Error: "Failed to create backup"

Solutions:

  1. Check disk space
  2. Verify backup directory exists and is writable
    $env:ANVIL_BACKUP_DIR = "D:\Backups\anvil"
    

4. Script Execution Issues

Script won't execute

Symptoms:

  • Error: "Script execution failed"
  • No output from script

Solutions:

  1. Verify script path in workload.yaml — paths are relative to scripts/

    scripts:
      post_install:
        - path: setup.ps1    # Relative to scripts/ directory
    
  2. Test script manually

    & "C:\workloads\my-workload\scripts\setup.ps1"
    

Execution policy error (Windows)

Symptoms:

  • Error: "Running scripts is disabled on this system"

Solutions:

  1. Set execution policy

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
    
  2. Check group policy — corporate environments may have stricter policies


Script timeout

Symptoms:

  • Error: "Script execution timed out"

Solutions:

  1. Increase timeout in workload

    scripts:
      post_install:
        - path: scripts/long-running.ps1
          timeout: 1800    # 30 minutes (default: 300)
    
  2. Check for infinite loops or slow network operations in the script


Elevated script fails (Windows)

Symptoms:

  • Error: "Elevation required"

Solutions:

  1. Run anvil as administrator
  2. Set the elevated flag in workload.yaml
    scripts:
      post_install:
        - path: scripts/admin-setup.ps1
          elevated: true
    

5. Health Check Issues

False failures

Symptoms:

  • Health check fails but software is working
  • Intermittent failures

Solutions:

  1. Run with verbose output

    anvil health my-workload --verbose
    
  2. Review health check scripts — they may have outdated expectations

  3. Update version expectations

    winget list --id Package.Name
    

Partial results

Symptoms:

  • Some checks don't run
  • Health report incomplete

Solutions:

  1. Check verbose output for errors in earlier checks

    anvil health my-workload -vvv
    
  2. Verify all referenced scripts exist in the workload directory


6. Configuration Issues

Config file not loading

Symptoms:

  • Settings not applied
  • Default values used instead of custom

Solutions:

  1. Check config location

    anvil config path
    
  2. View current configuration

    anvil config list
    
  3. Reset to defaults

    anvil config reset
    

Workloads not found

Symptoms:

  • Error: "Workload 'X' not found"
  • Empty list from anvil list

Solutions:

  1. Check search paths

    anvil config list
    
  2. Verify workload structure — needs a workload.yaml file

    workload-name/
    └── workload.yaml
    
  3. Use explicit path

    anvil install my-workload --path /path/to/workloads
    

7. Error Reference

Common Error Codes

ErrorDescriptionSolution
E001Workload not foundCheck name and search paths
E002Invalid workload schemaRun anvil validate for details
E003Circular dependencyReview extends chain in workloads
E004Package installation failedCheck package manager logs, verify package ID
E005File operation failedCheck permissions, verify paths
E006Script execution failedReview script output, check syntax
E007Health check failedReview check results, update scripts
E008Configuration errorValidate config file syntax
E009Backup operation failedCheck disk space and permissions
E010Restore operation failedVerify backup exists and is valid

8. Getting Help

Verbose Output

Get more detailed information about what Anvil is doing:

anvil -v <command>      # Some detail
anvil -vv <command>     # More detail
anvil -vvv <command>    # Maximum detail (debug level)

Enable Logging

# Set log level via environment variable
ANVIL_LOG=debug anvil install my-workload

# Available levels: error, warn, info, debug, trace

System Information

Gather helpful information when reporting issues:

# Anvil version
anvil --version

# Operating system
# Windows: winver
# Linux/macOS: uname -a

# Package manager version
winget --version        # Windows

Reporting Issues

When reporting issues, include:

  1. Anvil version: anvil --version
  2. Operating system and version
  3. Command that failed: exact command you ran
  4. Error message: complete error output
  5. Verbose output: run with -vvv flag
  6. Workload file: contents of workload.yaml (sanitized if needed)

Resources


Quick Reference

Common Commands for Troubleshooting

# Check Anvil installation
anvil --version

# Validate workload
anvil validate my-workload --strict

# Preview installation (dry run)
anvil install my-workload --dry-run

# Verbose health check
anvil health my-workload -vvv

# View current configuration
anvil config list

# Reset configuration
anvil config reset

Environment Variables

VariablePurpose
ANVIL_CONFIGCustom config file path
ANVIL_WORKLOADSAdditional workload search paths
ANVIL_LOGLog level (error, warn, info, debug, trace)
ANVIL_BACKUP_DIRCustom backup directory
NO_COLORDisable colored output

Specification

Version: 2.0.0
Status: Active
Last Updated: 2026-04-16

1. Executive Summary

1.1 Purpose

Anvil is a declarative workstation configuration management tool designed to automate the setup and validation of development environments. It provides a declarative approach to defining workstation configurations ("workloads") and offers both installation and health-check capabilities. Anvil currently targets Windows with winget as the primary package manager, with cross-platform support (Homebrew, APT) on the roadmap.

1.2 Core Modes of Operation

ModeDescription
InstallApply a workload configuration by installing packages, executing scripts, and copying files
Health CheckValidate the current system state against a workload definition

1.3 Key Features

  • Declarative Configuration: Define workloads in human-readable YAML files
  • Package Management: Leverage winget for software installation
  • File Synchronization: Copy configuration files to designated locations with integrity verification
  • Script Execution: Run pre/post installation scripts and diagnostic validations
  • Workload Composition: Inherit and extend base workloads for DRY configurations
  • Detailed Reporting: Comprehensive logs and health reports

2. Technology Analysis

2.1 Evaluation Criteria

CriterionWeightDescription
Windows Integration25%Native Windows APIs, winget interoperability, registry access
Maintainability20%Code clarity, testing support, community ecosystem
Extensibility15%Plugin architecture, workload composition
Dependencies15%Target machine requirements, deployment complexity
Performance10%Startup time, execution speed
Error Handling15%Exception management, detailed reporting

2.2 Technology Comparison

2.2.1 PowerShell Scripts

Strengths:

  • Native Windows integration; first-class citizen on all modern Windows
  • Direct winget command execution with native output parsing
  • No additional runtime dependencies
  • Excellent registry, file system, and Windows service access
  • Built-in remoting capabilities

Weaknesses:

  • Complex error handling patterns
  • Limited testing frameworks compared to general-purpose languages
  • Script organization becomes unwieldy at scale
  • Type system limitations for complex data structures
  • Inconsistent cross-version behavior (5.1 vs 7+)

Dependency Requirements: None (built into Windows)

Winget Integration: ★★★★★ Excellent - native command execution

Score: 72/100


2.2.2 Python Scripts

Strengths:

  • Rich ecosystem with excellent YAML/JSON/TOML parsing libraries
  • Strong testing frameworks (pytest, unittest)
  • Good readability and maintainability
  • Cross-platform potential
  • Excellent error handling with exceptions

Weaknesses:

  • Requires Python runtime installation on target machines
  • Virtual environment complexity for dependencies
  • Subprocess management for winget feels indirect
  • Windows-specific operations require additional libraries
  • Version compatibility issues (3.x versions)

Dependency Requirements: Python 3.8+ runtime, pip packages

Winget Integration: ★★★☆☆ Good - via subprocess

Score: 68/100


2.2.3 C# Script Files (dotnet-script)

Strengths:

  • Strong typing with excellent IDE support
  • Access to full .NET ecosystem
  • Good Windows integration via .NET APIs
  • Reasonable startup time for scripts
  • NuGet package support

Weaknesses:

  • Requires .NET SDK installation
  • Less common tooling; smaller community
  • Debugging experience inferior to full applications
  • Script organization patterns less established
  • Version pinning complexity

Dependency Requirements: .NET 6+ SDK, dotnet-script global tool

Winget Integration: ★★★☆☆ Good - via Process class

Score: 65/100


2.2.4 C# Console Application

Strengths:

  • Excellent type safety and refactoring support
  • Comprehensive .NET ecosystem (DI, logging, configuration)
  • Strong testing capabilities (xUnit, NUnit, MSTest)
  • Native Windows API access via P/Invoke
  • Can produce single-file executables
  • Mature error handling with structured exceptions
  • Excellent async/await support for parallel operations

Weaknesses:

  • Requires .NET runtime (can be self-contained but increases size)
  • Longer development cycle than scripting
  • Compilation step required
  • Larger deployment artifact if self-contained (~60MB+)

Dependency Requirements: .NET 8 runtime (or self-contained)

Winget Integration: ★★★★☆ Very Good - Process class with structured parsing

Score: 82/100


2.2.5 Rust CLI Application

Strengths:

  • Zero runtime dependencies; single static binary
  • Excellent performance and minimal resource usage
  • Strong type system with compile-time guarantees
  • Superior error handling with Result<T, E> pattern
  • Memory safety without garbage collection
  • Excellent CLI frameworks (clap, structopt)
  • Fast startup time

Weaknesses:

  • Steeper learning curve
  • Longer compilation times
  • Smaller Windows-specific ecosystem
  • Windows API interop more verbose than .NET
  • Less familiar to typical Windows administrators

Dependency Requirements: None (static binary)

Winget Integration: ★★★★☆ Very Good - std::process::Command with structured parsing

Score: 85/100


2.3 Comparison Matrix

CriterionPowerShellPythonC# ScriptC# AppRust
Windows Integration★★★★★★★★☆☆★★★★☆★★★★★★★★★☆
Maintainability★★★☆☆★★★★☆★★★☆☆★★★★★★★★★☆
Extensibility★★★☆☆★★★★☆★★★☆☆★★★★★★★★★★
Dependencies★★★★★★★☆☆☆★★☆☆☆★★★★☆★★★★★
Performance★★★☆☆★★★☆☆★★★☆☆★★★★☆★★★★★
Error Handling★★★☆☆★★★★☆★★★★☆★★★★★★★★★★
Weighted Score7268658285

3. Technology Recommendation

3.1 Primary Recommendation: Rust CLI Application

Rationale:

  1. Zero Dependencies: The single static binary eliminates runtime requirements, crucial for bootstrapping fresh Windows installations where development tools aren't yet installed.

  2. Robust Error Handling: Rust's Result<T, E> pattern enforces comprehensive error handling at compile time, preventing runtime surprises during critical system configuration.

  3. Performance: Fast startup (~5ms) and execution makes the tool feel responsive, encouraging frequent health checks.

  4. Configuration Parsing: Excellent libraries for YAML (serde_yaml), TOML (toml), and JSON (serde_json) with strong type safety.

  5. CLI Excellence: The clap crate provides industry-leading CLI parsing with automatic help generation, shell completions, and argument validation.

  6. Future-Proof: Rust's stability guarantees and backwards compatibility ensure long-term maintainability.

3.2 Configuration Format: YAML

Rationale:

  • Human-readable and writable without tooling
  • Supports comments for documentation
  • Native multi-line string support for inline scripts
  • Widely understood by developers and DevOps professionals
  • Excellent library support across all evaluated languages

3.3 Secondary Components: PowerShell

For pre/post installation scripts and health check validators, PowerShell remains the optimal choice:

  • Native Windows command execution
  • No additional dependencies
  • Familiar to Windows administrators
  • Direct access to Windows-specific features

3.4 Architecture Decision Records (ADR)

ADRDecisionRationale
ADR-001Use Rust for core CLIZero runtime deps, robust error handling
ADR-002Use YAML for workload definitionsHuman-readable, supports comments
ADR-003Use PowerShell for scriptsNative Windows integration
ADR-004Use SHA-256 for file hashingIndustry standard, good performance
ADR-005Support workload inheritanceDRY principle, modular configurations

4. Project Structure

4.1 Repository Layout

anvil/
├── .github/
│   └── workflows/
│       ├── ci.yml                    # Continuous integration
│       └── release.yml               # Release automation
├── docs/
│   ├── SPECIFICATION.md              # This document
│   ├── USER_GUIDE.md                 # End-user documentation
│   ├── WORKLOAD_AUTHORING.md         # Guide for creating workloads
│   └── TROUBLESHOOTING.md            # Common issues and solutions
├── src/
│   ├── main.rs                       # Application entry point
│   ├── cli/
│   │   ├── mod.rs                    # CLI module root
│   │   ├── commands.rs               # Command definitions
│   │   └── output.rs                 # Output formatting (table, JSON)
│   ├── config/
│   │   ├── mod.rs                    # Configuration module root
│   │   ├── workload.rs               # Workload struct definitions
│   │   ├── schema.rs                 # Schema validation
│   │   └── inheritance.rs            # Workload composition logic
│   ├── operations/
│   │   ├── mod.rs                    # Operations module root
│   │   ├── install.rs                # Installation operations
│   │   ├── health.rs                 # Health check operations
│   │   └── backup.rs                 # Backup/restore operations
│   ├── providers/
│   │   ├── mod.rs                    # Providers module root
│   │   ├── winget.rs                 # Winget package manager
│   │   ├── filesystem.rs             # File operations
│   │   └── script.rs                 # Script execution
│   ├── hashing/
│   │   ├── mod.rs                    # Hashing module root
│   │   └── sha256.rs                 # SHA-256 implementation
│   └── reporting/
│       ├── mod.rs                    # Reporting module root
│       ├── console.rs                # Console output
│       ├── json.rs                   # JSON report generation
│       └── html.rs                   # HTML report generation
├── workloads/
│   ├── essentials/
│   │   ├── workload.yaml             # Core dev tools (VS Code, Git, Terminal) and utilities
│   │   ├── files/
│   │   │   └── user/                 # User configuration files
│   │   └── scripts/
│   │       ├── health-check.ps1      # Essentials health validation
│   │       └── configure-sudo.ps1    # Windows sudo configuration
│   ├── rust-developer/
│   │   ├── workload.yaml             # Rust toolchain workload
│   │   ├── files/
│   │   │   └── config.toml           # Cargo config
│   │   └── scripts/
│   │       ├── post-install.ps1      # Rustup component installation
│   │       └── health-check.ps1      # Rust environment validation
│   └── python-developer/
│       ├── workload.yaml             # Python development workload
│       ├── files/
│       │   └── pip.ini               # Pip configuration
│       └── scripts/
│           ├── post-install.ps1      # Virtual environment setup
│           └── health-check.ps1      # Python environment validation
├── tests/
│   ├── integration/
│   │   ├── install_tests.rs          # Installation integration tests
│   │   └── health_tests.rs           # Health check integration tests
│   └── fixtures/
│       └── sample-workload/          # Test workload
├── Cargo.toml                        # Rust project manifest
├── Cargo.lock                        # Dependency lock file
├── README.md                         # Project overview
├── LICENSE                           # License file
└── .gitignore                        # Git ignore rules

4.2 Workload Directory Structure

Each workload follows a consistent structure:

<workload-name>/
├── workload.yaml           # Workload definition (required)
├── files/                  # Files to copy to target system (optional)
│   ├── .config/
│   │   └── app/
│   │       └── settings.json
│   └── .profile
└── scripts/                # Scripts for installation/validation (optional)
    ├── pre-install.ps1     # Runs before package installation
    ├── post-install.ps1    # Runs after package installation
    └── health-check.ps1    # Validation script for health mode

5. Workload Definition Schema

5.1 Schema Overview (YAML)

# workload.yaml - Full Schema Reference

# Metadata (required)
name: string                    # Unique workload identifier
version: string                 # Semantic version (e.g., "1.0.0")
description: string             # Human-readable description

# Inheritance (optional)
extends:                        # List of parent workloads
  - string                      # Parent workload name

# Package Management (optional)
packages:
  winget:                       # Winget package definitions
    - id: string                # Winget package ID (required)
      version: string           # Specific version (optional, default: latest)
      source: string            # Package source (optional, default: winget)
      override: string[]        # Additional winget arguments (optional)
      
# File Management (optional)
files:
  - source: string              # Relative path from workload's files/ directory
    destination: string         # Absolute path or path with variables
    backup: boolean             # Backup existing file (optional, default: true)
    permissions: string         # File permissions (optional, future use)
    template: boolean           # Process as template (optional, default: false)
    
# Script Execution (optional)
scripts:
  pre_install:                  # Scripts to run before installation
    - path: string              # Relative path from workload's scripts/ directory
      shell: string             # Execution shell (optional, default: "powershell")
      elevated: boolean         # Require admin privileges (optional, default: false)
      timeout: integer          # Timeout in seconds (optional, default: 300)
      
  post_install:                 # Scripts to run after installation
    - path: string
      shell: string
      elevated: boolean
      timeout: integer
      
  health_check:                 # Scripts for health validation
    - path: string
      shell: string
      name: string              # Display name for the check
      description: string       # What this check validates
      
# Environment Configuration (optional)
environment:
  variables:                    # Environment variables to set
    - name: string              # Variable name
      value: string             # Variable value
      scope: string             # "user" or "machine" (default: "user")
      
  path_additions:               # Additions to PATH
    - string                    # Path to add

# Health Check Configuration (optional)
health:
  package_check: boolean        # Verify packages installed (default: true)
  file_check: boolean           # Verify files match (default: true)
  script_check: boolean         # Run health check scripts (default: true)
  assertion_check: boolean      # Evaluate declarative assertions (default: true)

5.1.1 Assertions and Legacy Health Check Coexistence

Anvil supports two mechanisms for health validation:

MechanismFieldIntroducedStatus
Declarative assertionsassertions:v0.5Recommended
Health check scriptsscripts.health_checkv0.1Deprecated when used with assertions

Execution order: When both exist, assertions run first, then legacy scripts. Results are merged into a single health report.

Deprecation timeline:

  • v0.5: Warning emitted when scripts.health_check is used alongside assertions
  • v0.6: Warning emitted for any use of scripts.health_check (even without assertions)
  • v1.0: scripts.health_check removed; use assertions exclusively

Migration: Convert health check scripts to declarative assertions:

# Before (legacy script)
scripts:
  health_check:
    - path: check-git.ps1
      name: "Git installed"

# After (declarative assertion)
assertions:
  - name: Git installed
    check:
      type: command_exists
      command: git

5.1.2 Commands and Legacy Scripts Coexistence

Anvil supports two mechanisms for running actions during installation:

MechanismFieldIntroducedStatus
Inline commandscommands.pre_install / commands.post_installv0.6Recommended
Script filesscripts.pre_install / scripts.post_installv0.1Deprecated when used with commands

Execution order: When both exist for the same phase, commands run first, then legacy scripts.

Deprecation timeline:

  • v0.6: Warning emitted when scripts.pre_install/scripts.post_install is used alongside commands.pre_install/commands.post_install
  • v0.8: Warning emitted for any use of scripts.pre_install/scripts.post_install
  • v1.0: scripts.pre_install and scripts.post_install removed; use commands exclusively

Migration: Convert script entries to inline commands:

# Before (legacy script)
scripts:
  post_install:
    - path: setup.ps1
      description: "Install Rust components"

# After (inline command)
commands:
  post_install:
    - run: rustup default stable
      description: "Set stable as default toolchain"
    - run: cargo install cargo-watch
      description: "Install cargo-watch"
      when:
        type: command_exists
        command: cargo

Note: scripts.health_check follows a separate deprecation path (see §5.1.1).

5.1.3 Command Execution Semantics

Commands defined in commands.pre_install and commands.post_install are executed inline during the install flow.

Execution rules:

  • Commands run sequentially in the order defined
  • Default behavior: stop on first failure (non-zero exit code)
  • continue_on_error: true overrides this per-command
  • Commands with when: conditions are evaluated before execution; if the condition fails, the command is skipped

Output handling:

  • stdout and stderr are captured and included in reports
  • In verbose mode (-v), output is streamed in real-time
  • In quiet mode (-q), output is suppressed unless the command fails

Timeout behavior:

  • Default timeout: 300 seconds
  • On timeout: process is terminated (SIGTERM on Unix, TerminateProcess on Windows)
  • Timed-out commands are reported as failures

Elevated commands:

  • Commands with elevated: true require admin privileges
  • On Windows: validated via whoami /priv or equivalent before attempting
  • If elevation is unavailable, the command fails with a clear error message

Exit codes:

  • Exit code 0 = success
  • Any non-zero exit code = failure (unless continue_on_error: true)

5.2 Example Workloads

5.2.1 Essentials Workload

# workloads/essentials/workload.yaml
name: essentials
version: "2.0.0"
description: "Core development tools and productivity utilities for everyday Windows use"

packages:
  winget:
    - id: Git.Git
      override:
        - --override
        - '/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"'
    
    - id: Microsoft.VisualStudioCode
      override:
        - --override
        - '/VERYSILENT /NORESTART /MERGETASKS=!runcode,addcontextmenufiles,addcontextmenufolders,addtopath'
    
    - id: Microsoft.WindowsTerminal
    
    - id: JanDeDobbeleer.OhMyPosh
    
    - id: GitHub.cli

files:
  - source: user/.gitconfig
    destination: "~/.gitconfig"
    backup: true
    
  - source: user/.config/pwsh
    destination: "~/.config/pwsh"
    backup: true

scripts:
  post_install:
    - path: post-install.ps1
      shell: powershell
    - path: configure-sudo.ps1
      shell: powershell
      elevated: true
      
  health_check:
    - path: health-check.ps1
      name: "essentials Health Check"
      description: "Verifies essentials is properly configured"
    - path: verify-sudo.ps1
      name: "Windows Sudo"
      description: "Verifies Windows sudo is enabled and set to inline mode"

5.2.2 Rust Developer Workload

# workloads/rust-developer/workload.yaml
name: rust-developer
version: "1.0.0"
description: "Complete Rust development environment"

extends:
  - essentials

packages:
  winget:
    - id: Rustlang.Rustup
    
    - id: Microsoft.VisualStudio.2022.BuildTools
      override:
        - --override
        - '--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
    
    - id: LLVM.LLVM

files:
  - source: config.toml
    destination: "~/.cargo/config.toml"
    backup: true
    
  - source: rustfmt.toml
    destination: "~/rustfmt.toml"
    backup: false

scripts:
  post_install:
    - path: post-install.ps1
      shell: powershell
      description: "Install Rust components and common tools"
      timeout: 600
      
  health_check:
    - path: health-check.ps1
      name: "Rust Toolchain"
      description: "Verifies rustc, cargo, and components are properly installed"

environment:
  path_additions:
    - "~/.cargo/bin"

5.2.3 Python Developer Workload

# workloads/python-developer/workload.yaml
name: python-developer
version: "1.0.0"
description: "Python development environment with common tools"

extends:
  - essentials

packages:
  winget:
    - id: Python.Python.3.12
    
    - id: astral-sh.uv

files:
  - source: pip.ini
    destination: "~/AppData/Roaming/pip/pip.ini"
    backup: true

scripts:
  post_install:
    - path: post-install.ps1
      shell: powershell
      description: "Configure Python environment and install global tools"
      
  health_check:
    - path: health-check.ps1
      name: "Python Environment"
      description: "Verifies Python, pip, and uv are properly configured"

environment:
  variables:
    - name: PYTHONDONTWRITEBYTECODE
      value: "1"
      scope: user

5.3 Variable Expansion

Anvil supports variable expansion in destination paths and template files:

VariableExpansion
~User's home directory (%USERPROFILE%)
${HOME}User's home directory
${APPDATA}Application data directory
${LOCALAPPDATA}Local application data
${PROGRAMFILES}Program Files directory
${PROGRAMFILES_X86}Program Files (x86) directory
${ANVIL_WORKLOAD}Current workload name
${ANVIL_VERSION}Anvil version

6. CLI Interface Design

6.1 Command Overview

anvil <COMMAND> [OPTIONS]

Commands:
  install     Apply a workload configuration to the system
  health      Check system health against a workload definition
  list        List available workloads
  show        Display detailed workload information
  validate    Validate workload definition syntax
  init        Initialize a new workload template
  backup      Backup current system state
  restore     Restore from a backup
  completions Generate shell completions
  help        Print help information

Options:
  -v, --verbose       Increase output verbosity (can be repeated: -vvv)
  -q, --quiet         Suppress non-essential output
  -c, --config <PATH> Use custom configuration file
  --no-color          Disable colored output
  -h, --help          Print help
  -V, --version       Print version

6.2 Command Details

6.2.1 Install Command

anvil install [OPTIONS] <WORKLOAD>

Apply a workload configuration to the system

Arguments:
  <WORKLOAD>  Name of the workload to install (or path to workload.yaml)

Options:
  -d, --dry-run           Show what would be done without making changes
  -f, --force             Skip confirmation prompts
  -p, --packages-only     Only install packages, skip files and scripts
  --skip-packages         Skip package installation
  --skip-files            Skip file operations
  --skip-scripts          Skip script execution
  --skip-pre-scripts      Skip pre-installation scripts
  --skip-post-scripts     Skip post-installation scripts
  --no-backup             Don't backup existing files
  -j, --jobs <N>          Number of parallel package installations (default: 4)
  --timeout <SECONDS>     Global timeout for operations (default: 3600)
  
Examples:
  anvil install rust-developer
  anvil install ./custom-workload/workload.yaml
  anvil install python-developer --dry-run
  anvil install essentials --packages-only

6.2.2 Health Command

anvil health [OPTIONS] <WORKLOAD>

Check system health against a workload definition

Arguments:
  <WORKLOAD>  Name of the workload to check against

Options:
  -o, --output <FORMAT>   Output format: table, json, yaml, html (default: table)
  -f, --file <PATH>       Write report to file instead of stdout
  --fail-fast             Stop on first failure
  --packages-only         Only check packages
  --files-only            Only check files
  --scripts-only          Only run health check scripts
  -s, --strict            Treat warnings as errors
  
Exit Codes:
  0  All checks passed
  1  One or more checks failed
  2  Configuration or runtime error

Examples:
  anvil health rust-developer
  anvil health python-developer --output json --file report.json
  anvil health essentials --fail-fast

6.2.3 List Command

anvil list [OPTIONS]

List available workloads

Options:
  -a, --all               Include built-in and custom workloads
  -l, --long              Show detailed information
  --path <PATH>           Search for workloads in additional path
  -o, --output <FORMAT>   Output format: table, json, yaml (default: table)

Examples:
  anvil list
  anvil list --long
  anvil list --output json

6.2.4 Show Command

anvil show <WORKLOAD>

Display detailed workload information

Arguments:
  <WORKLOAD>  Name of the workload to display

Options:
  -r, --resolved          Show resolved configuration (with inheritance applied)
  -o, --output <FORMAT>   Output format: yaml, json (default: yaml)

Examples:
  anvil show rust-developer
  anvil show python-developer --resolved

6.2.5 Validate Command

anvil validate [OPTIONS] <PATH>

Validate workload definition syntax

Arguments:
  <PATH>  Path to workload.yaml file or workload directory

Options:
  --strict               Enable strict validation mode
  --schema               Output JSON schema for workload definitions

Examples:
  anvil validate workloads/rust-developer/
  anvil validate ./custom-workload/workload.yaml --strict

6.2.6 Init Command

anvil init [OPTIONS] <NAME>

Initialize a new workload template

Arguments:
  <NAME>  Name for the new workload

Options:
  -t, --template <NAME>   Base template: minimal, standard, full (default: standard)
  -e, --extends <PARENT>  Parent workload to extend
  -o, --output <PATH>     Output directory (default: ./workloads/<NAME>)

Examples:
  anvil init my-workload
  anvil init frontend-dev --extends essentials
  anvil init minimal-setup --template minimal

6.3 Output Formats

6.3.1 Table Format (Default)

$ anvil health rust-developer

╭─────────────────────────────────────────────────────────────────────╮
│                    Anvil Health Check Report                      │
│                    Workload: rust-developer                          │
├─────────────────────────────────────────────────────────────────────┤
│ Component          │ Status  │ Details                              │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Packages                                                             │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Git.Git            │ ✓ OK    │ 2.43.0 installed                     │
│ Microsoft.VSCode   │ ✓ OK    │ 1.85.0 installed                     │
│ Rustlang.Rustup    │ ✓ OK    │ 1.26.0 installed                     │
│ LLVM.LLVM          │ ✗ FAIL  │ Not installed                        │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Files                                                                │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ ~/.cargo/config    │ ✓ OK    │ Hash matches                         │
│ ~/.gitconfig       │ ⚠ WARN  │ Modified locally                     │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Health Scripts                                                       │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Rust Toolchain     │ ✓ OK    │ All components verified              │
│ Git Configuration  │ ✓ OK    │ User configured                      │
╰─────────────────────────────────────────────────────────────────────╯

Summary: 6 passed, 1 failed, 1 warning

6.3.2 JSON Format

{
  "workload": "rust-developer",
  "timestamp": "2025-01-15T10:30:00Z",
  "overall_status": "FAILED",
  "summary": {
    "total": 8,
    "passed": 6,
    "failed": 1,
    "warnings": 1
  },
  "checks": {
    "packages": [
      {
        "id": "Git.Git",
        "status": "OK",
        "expected_version": null,
        "installed_version": "2.43.0"
      },
      {
        "id": "LLVM.LLVM",
        "status": "FAILED",
        "expected_version": null,
        "installed_version": null,
        "error": "Package not installed"
      }
    ],
    "files": [
      {
        "source": ".cargo/config.toml",
        "destination": "C:\\Users\\dev\\.cargo\\config.toml",
        "status": "OK",
        "expected_hash": "sha256:abc123...",
        "actual_hash": "sha256:abc123..."
      }
    ],
    "scripts": [
      {
        "name": "Rust Toolchain",
        "status": "OK",
        "output": "rustc 1.75.0, cargo 1.75.0"
      }
    ]
  }
}

7. Core Components

7.1 Component Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                              CLI Layer                               │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐   │
│  │ install │ │ health  │ │  list   │ │  show   │ │  validate   │   │
│  └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬──────┘   │
└───────┼──────────┼────────────┼──────────┼─────────────┼───────────┘
        │          │            │          │             │
┌───────┴──────────┴────────────┴──────────┴─────────────┴───────────┐
│                          Operations Layer                           │
│  ┌──────────────────┐  ┌──────────────────┐  ┌────────────────┐    │
│  │ InstallOperation │  │ HealthOperation  │  │ BackupOperation│    │
│  └────────┬─────────┘  └────────┬─────────┘  └───────┬────────┘    │
└───────────┼─────────────────────┼────────────────────┼─────────────┘
            │                     │                    │
┌───────────┴─────────────────────┴────────────────────┴─────────────┐
│                          Providers Layer                            │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐     │
│  │ WingetProvider  │  │ FilesystemProv. │  │ ScriptProvider  │     │
│  │                 │  │                 │  │                 │     │
│  │ - install()     │  │ - copy()        │  │ - execute()     │     │
│  │ - uninstall()   │  │ - compare()     │  │ - validate()    │     │
│  │ - list()        │  │ - backup()      │  │                 │     │
│  │ - search()      │  │ - restore()     │  │                 │     │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘     │
└────────────────────────────────────────────────────────────────────┘
            │                     │                    │
┌───────────┴─────────────────────┴────────────────────┴─────────────┐
│                          Config Layer                               │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐     │
│  │ WorkloadParser  │  │ SchemaValidator │  │ InheritanceRes. │     │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘     │
└────────────────────────────────────────────────────────────────────┘

7.2 Winget Provider

The Winget Provider interfaces with Windows Package Manager:

Key Functions:

FunctionDescription
install(package)Install a package with optional version/overrides
uninstall(package)Remove an installed package
is_installed(package)Check if a package is installed
get_version(package)Get installed version of a package
list_installed()List all installed packages
search(query)Search for packages

Error Handling:

#![allow(unused)]
fn main() {
pub enum WingetError {
    PackageNotFound(String),
    InstallationFailed { package: String, exit_code: i32, stderr: String },
    VersionMismatch { package: String, expected: String, actual: String },
    NetworkError(String),
    AccessDenied(String),
    Timeout { package: String, timeout_seconds: u64 },
}
}

7.3 Filesystem Provider

Handles file operations with integrity verification:

Key Functions:

FunctionDescription
copy_file(src, dest)Copy file with optional backup
compute_hash(path)Calculate SHA-256 hash
compare_files(a, b)Compare files by hash
backup_file(path)Create timestamped backup
restore_file(backup, dest)Restore from backup
expand_path(path)Expand variables in path

Hash Format:

sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

7.4 Script Provider

Executes PowerShell scripts with proper error handling:

Key Functions:

FunctionDescription
execute(script, config)Run a script with configuration
validate_syntax(script)Check script for syntax errors
run_elevated(script)Execute with admin privileges

Script Execution Model:

#![allow(unused)]
fn main() {
pub struct ScriptConfig {
    pub path: PathBuf,
    pub shell: Shell,           // PowerShell, Cmd, Bash (WSL)
    pub elevated: bool,
    pub timeout: Duration,
    pub working_dir: Option<PathBuf>,
    pub environment: HashMap<String, String>,
}

pub struct ScriptResult {
    pub exit_code: i32,
    pub stdout: String,
    pub stderr: String,
    pub duration: Duration,
    pub success: bool,
}
}

7.5 Inheritance Resolution

Workloads can extend other workloads with the following merge rules:

FieldMerge Strategy
nameChild overwrites
versionChild overwrites
descriptionChild overwrites
packages.wingetAppend (child packages added after parent)
filesAppend (child files added, same destinations overwritten)
scripts.pre_installParent first, then child
scripts.post_installParent first, then child
scripts.health_checkCombine all
environment.variablesChild overwrites same-named variables
environment.path_additionsAppend

Circular Dependency Detection:

The inheritance resolver maintains a visited set to detect and reject circular dependencies:

A extends B extends C extends A  →  ERROR: Circular dependency detected

8. Roadmap

Phases 1–6 of the original implementation are complete. This section describes the forward-looking roadmap for Anvil, organized by milestone.

8.1 Overview

v0.4 — Declarative Assertions
    │
    ├── Reusable condition/predicate engine
    ├── assertions: YAML schema
    ├── Integration with health command
    └── Backward compatibility with scripts.health_check

v0.5 — Package Manager Abstraction
    │
    ├── PackageManager trait
    ├── WingetProvider adapter
    └── Schema design for multi-manager support

v0.6 — Commands Block
    │
    ├── commands: YAML schema
    ├── Conditional execution (when:)
    ├── Failure semantics
    └── Backward compatibility with scripts.post_install

v0.7 — Workload Discovery & Separation
    │
    ├── Wire search_paths through ConfigManager
    ├── Search precedence and conflict resolution
    └── Private workload repository pattern

v1.0 — Polish & Distribution
    │
    ├── crates.io publishing
    ├── Cross-platform CI
    ├── Remove scripts.health_check (use assertions exclusively)
    └── Documentation review

8.2 v0.4 — Declarative Assertions

Goal: Replace ~70% of PowerShell health-check scripts with declarative YAML assertions, evaluated natively in Rust.

Before (current):

scripts:
  health_check:
    - path: health-check/check-rust.ps1
      name: "Rust Toolchain"

Plus a 70-line PowerShell script.

After (proposed):

assertions:
  - name: "Rust compiler"
    check:
      type: command_exists
      command: rustc
      min_version: "1.70"
  - name: "Cargo config"
    check:
      type: file_exists
      path: "~/.cargo/config.toml"
  - name: "RUST_BACKTRACE set"
    check:
      type: env_var
      name: RUST_BACKTRACE
      value: "1"

Assertion types:

TypeParametersPurpose
command_existscommand, min_version?Verify a CLI tool is installed
file_existspathVerify a file exists
dir_existspathVerify a directory exists
env_varname, value?Verify an environment variable is set
path_containsentryVerify PATH includes an entry
json_propertypath, property, valueVerify a JSON file property
registry_valuekey, name, valueVerify a Windows registry value
shellcommand, expected_exit_code?Escape hatch — run a shell command
all_ofchecks: [...]All sub-checks must pass (AND)
any_ofchecks: [...]At least one sub-check must pass (OR)

Implementation:

  • New module: src/conditions/ — shared predicate engine reused by assertions and future commands.when
  • New module: src/assertions/Assertion enum (tagged serde), evaluate() function
  • Update src/config/workload.rs — add assertions field to Workload
  • Update src/operations/health.rs — evaluate assertions alongside legacy scripts.health_check
  • Backward compatible — existing scripts.health_check continues to work

8.3 v0.5 — Package Manager Abstraction

Goal: Introduce a PackageManager trait so the install flow is decoupled from winget, enabling future cross-platform support.

Proposed trait:

#![allow(unused)]
fn main() {
pub trait PackageManager: Send + Sync {
    fn name(&self) -> &str;
    fn is_available(&self) -> bool;
    fn install(&self, package: &PackageSpec) -> Result<InstallResult>;
    fn is_installed(&self, package_id: &str) -> Result<Option<String>>;
    fn list_installed(&self) -> Result<Vec<InstalledPackage>>;
}
}

Schema direction:

packages:
  winget:
    - id: Microsoft.VisualStudioCode
  brew:
    - id: visual-studio-code
  apt:
    - id: code

Anvil selects the available manager for the current platform. The v0.5 milestone implements the trait and refactors WingetProvider — other manager implementations are deferred.

8.4 v0.6 — Commands Block

Goal: Replace post-install PowerShell scripts with inline commands that support conditional execution.

Proposed schema:

commands:
  post_install:
    - run: rustup default stable
      description: "Set stable as default toolchain"
    - run: cargo install cargo-watch
      timeout: 900
    - run: code --install-extension rust-lang.rust-analyzer
      when:
        command_exists: code

Failure semantics:

  • Default: stop on first failure
  • continue_on_error: true to continue
  • Non-zero exit codes are errors unless expected_exit_code is set
  • Commands produce structured output for reporting

The when: condition reuses the predicate engine from v0.4.

8.5 v0.7 — Workload Discovery & Separation

Goal: Support multiple workload directories so private workloads live outside the main repo.

Current state: GlobalConfig already has a workloads.paths field, but ConfigManager uses only default_workload_paths(). This milestone wires configured paths through all commands and defines precedence.

Search precedence:

  1. Explicit path argument (highest priority)
  2. User-configured search paths (~/.anvil/config.yaml)
  3. Default paths (exe-relative, LOCALAPPDATA, cwd)

Duplicate workload names produce a warning with the resolved path shown.

8.6 v1.0 — Polish & Distribution

Goal: Stable release with cross-platform CI and crates.io publishing.

  • Decide crate name (anvil is taken on crates.io — candidates: anvil-cli, devsmith, forgekit)
  • Cross-compilation CI for Windows, Linux, macOS
  • Comprehensive documentation review
  • First stable release

9. Appendices

9.1 Sample Health Check Script

# health-check.ps1 - Rust Developer Workload

$ErrorActionPreference = "Stop"
$exitCode = 0

function Test-Command {
    param([string]$Command, [string]$Name)
    
    try {
        $null = Get-Command $Command -ErrorAction Stop
        Write-Host "✓ $Name is available" -ForegroundColor Green
        return $true
    }
    catch {
        Write-Host "✗ $Name is not available" -ForegroundColor Red
        return $false
    }
}

function Test-RustComponent {
    param([string]$Component)
    
    $installed = rustup component list --installed 2>&1
    if ($installed -match $Component) {
        Write-Host "✓ Rust component '$Component' is installed" -ForegroundColor Green
        return $true
    }
    else {
        Write-Host "✗ Rust component '$Component' is missing" -ForegroundColor Red
        return $false
    }
}

# Check core tools
if (-not (Test-Command "rustc" "Rust Compiler")) { $exitCode = 1 }
if (-not (Test-Command "cargo" "Cargo")) { $exitCode = 1 }
if (-not (Test-Command "rustup" "Rustup")) { $exitCode = 1 }

# Check Rust version
$rustVersion = rustc --version
Write-Host "Rust version: $rustVersion"

# Check essential components
if (-not (Test-RustComponent "rust-src")) { $exitCode = 1 }
if (-not (Test-RustComponent "rust-analyzer")) { $exitCode = 1 }
if (-not (Test-RustComponent "clippy")) { $exitCode = 1 }
if (-not (Test-RustComponent "rustfmt")) { $exitCode = 1 }

# Check cargo tools
if (-not (Test-Command "cargo-watch" "cargo-watch")) { 
    Write-Host "⚠ cargo-watch not installed (optional)" -ForegroundColor Yellow 
}

exit $exitCode

9.2 Sample Post-Install Script

# post-install.ps1 - Rust Developer Workload

$ErrorActionPreference = "Stop"

Write-Host "Configuring Rust development environment..." -ForegroundColor Cyan

# Update Rust toolchain
Write-Host "Updating Rust toolchain..."
rustup update

# Install stable toolchain
Write-Host "Installing stable toolchain..."
rustup default stable

# Install essential components
Write-Host "Installing Rust components..."
rustup component add rust-src
rustup component add rust-analyzer
rustup component add clippy
rustup component add rustfmt

# Install common cargo tools
Write-Host "Installing cargo tools..."
cargo install cargo-watch
cargo install cargo-edit
cargo install cargo-expand

# Configure VS Code (if installed)
$vscodePath = Get-Command code -ErrorAction SilentlyContinue
if ($vscodePath) {
    Write-Host "Installing VS Code extensions for Rust..."
    code --install-extension rust-lang.rust-analyzer
    code --install-extension vadimcn.vscode-lldb
    code --install-extension serayuzgur.crates
}

Write-Host "Rust development environment configured successfully!" -ForegroundColor Green

9.3 Error Codes Reference

CodeNameDescription
0SUCCESSOperation completed successfully
1HEALTH_CHECK_FAILEDOne or more health checks failed
2CONFIG_ERRORConfiguration file error
3WORKLOAD_NOT_FOUNDSpecified workload does not exist
4WINGET_ERRORWinget operation failed
5FILE_ERRORFile operation failed
6SCRIPT_ERRORScript execution failed
7PERMISSION_ERRORInsufficient permissions
8NETWORK_ERRORNetwork connectivity issue
9TIMEOUTOperation timed out
10CIRCULAR_DEPENDENCYCircular workload inheritance

9.4 Glossary

TermDefinition
WorkloadA named configuration bundle defining packages, files, and scripts
Health CheckValidation of system state against a workload definition
ProviderComponent that interfaces with external systems (winget, filesystem, scripts)
InheritanceMechanism for workloads to extend and build upon other workloads
Variable ExpansionReplacement of placeholders like ~ or ${HOME} with actual paths

Document History

VersionDateAuthorChanges
2.0.02026-04-16Anvil TeamRename to Anvil, updated roadmap (v0.4–v1.0)
1.0.02025-01-15Anvil TeamInitial specification

This specification serves as the authoritative reference for implementing Anvil. Subsequent implementation prompts should reference this document for architecture decisions, schemas, and interfaces.

Architecture

This document describes Anvil's internal architecture for contributors. For user-facing documentation, see the User Guide. For the project vision and roadmap, see the Specification.

Overview

Anvil is a single-binary Rust CLI. The binary parses commands, loads YAML workload definitions, delegates work to providers, and tracks state on disk.

main.rs → cli/ → operations/ → providers/
                      ↓              ↓
                   config/        state/
LayerRole
cli/Command parsing (clap), output formatting (table/JSON/YAML/HTML), progress bars
config/Workload YAML loading, schema validation, inheritance resolution, global config
operations/One module per CLI command — each exposes execute(args, cli) → Result<()>
providers/External system integrations: winget, filesystem, scripts, templates, backups
state/Tracks installation records, file hashes, and package cache at ~/.anvil/

Data Flow: anvil install <workload>

1. CLI parsing          cli/mod.rs        → InstallArgs
2. Workload discovery   config/mod.rs     → finds workload.yaml on search paths
3. YAML loading         config/mod.rs     → Workload struct (serde)
4. Inheritance          config/inheritance.rs → resolved Workload (parents merged)
5. Variable expansion   config/mod.rs     → ~ and ${VAR} expanded
6. Plan & confirm       operations/install.rs → dry-run plan, user confirmation
7. Package install      providers/winget.rs   → winget install per package
8. File copy            providers/filesystem.rs → copy with backup and hash
9. Script execution     providers/script.rs   → PowerShell pre/post scripts
10. State persistence   state/              → JSON files at ~/.anvil/state/

Key Types

Workload Schema (config/workload.rs)

#![allow(unused)]
fn main() {
pub struct Workload {
    pub name: String,
    pub version: String,
    pub description: String,
    pub extends: Option<Vec<String>>,
    pub packages: Option<Packages>,
    pub files: Option<Vec<FileEntry>>,
    pub scripts: Option<Scripts>,
    pub environment: Option<Environment>,
    pub health: Option<HealthConfig>,
}
}

The Workload struct is the central data type — deserialized from YAML via serde. All fields except name, version, and description are optional.

CLI (cli/mod.rs, cli/commands.rs)

10 top-level commands: install, health, list, show, validate, init, status, completions, backup, config.

The Cli struct (clap-derived) holds global flags (--verbose, --quiet, --no-color, --config). Each command has its own args struct.

Providers (providers/)

Providers wrap external systems. Each provider is a struct with methods — there is no shared trait yet (see roadmap).

ProviderResponsibility
WingetProviderPackage install/upgrade/query via winget.exe
FilesystemProviderFile copy with backup, hash verification, glob expansion
ScriptProviderPowerShell script execution with timeout and output capture
TemplateProcessorHandlebars template rendering for config files
BackupManagerSystem state backup and restore

State (state/)

All state is persisted as JSON under ~/.anvil/:

~/.anvil/
├── state/
│   ├── <workload>.json      # InstallationState per workload
│   └── files.json           # FileStateIndex (all tracked files)
├── cache/
│   └── packages.json        # PackageCache (winget query results)
└── config.yaml              # GlobalConfig (user preferences)
TypeFilePurpose
InstallationStatestate/<workload>.jsonPackage install records and status
FileStateManagerstate/files.jsonFile hashes for drift detection
PackageCachecache/packages.jsonCached winget query results
GlobalConfigconfig.yamlUser settings and search paths

Config System

Workload Discovery

ConfigManager searches for workloads in this order:

  1. Direct path (if an absolute or relative path is given)
  2. <exe_dir>/workloads/<name>/workload.yaml
  3. %LOCALAPPDATA%/anvil/workloads/<name>/workload.yaml
  4. ./workloads/<name>/workload.yaml

Within each search path, it tries: <name>/workload.yaml, <name>/workload.yml, <name>.yaml.

Inheritance

Workloads can extend other workloads via extends: [parent]. Resolution in config/inheritance.rs:

  1. Build dependency graph from all extends references
  2. Detect cycles (error) and enforce max depth of 10
  3. Topological sort determines merge order (parents first)
  4. Merge strategy:
    • Packages: append, child overrides same ID
    • Files: append, child overrides same destination
    • Scripts: concatenate (parent first, child after)
    • Environment variables: child overrides same name
    • Path additions: append unique entries

Variable Expansion

Workload values support variable expansion:

VariableExpands to
~$env:USERPROFILE
${HOME}$env:USERPROFILE
${ANVIL_WORKLOAD}Current workload name
${ANVIL_VERSION}Anvil version
${ANVIL_WORKLOAD_PATH}Path to workload directory
${ENV_NAME}Any environment variable

Error Handling

Two-tier approach:

  • thiserror for domain-specific error enums in each module:
    • WingetError, FilesystemError, ScriptError, BackupError, TemplateError
    • InheritanceError (includes suggestion() for user-friendly hints)
    • FileStateError
    • ProviderError (wraps all provider errors)
  • anyhow for error propagation in operations and CLI code

Pattern: domain errors are created with thiserror and converted to anyhow::Error at operation boundaries using .with_context(|| ...).

Testing

Unit Tests

Inline #[cfg(test)] mod tests in the same file as the code under test. 168 unit tests covering providers, config parsing, inheritance, state management, and formatting.

Integration Tests

tests/cli_tests.rs uses assert_cmd + predicates for end-to-end CLI testing. 75 integration tests covering all commands with fixture workloads.

tests/common/mod.rs provides test fixture helpers:

  • create_test_workload() — minimal valid workload
  • create_inherited_workload() — parent + child workloads
  • create_invalid_workload() — malformed YAML
  • create_circular_workloads() — cycle detection fixtures
  • create_full_workload() — workload with all features
  • create_template_workload() — workload with template files

Running Tests

cargo test                   # All tests (243 total)
cargo test --bin anvil       # Unit tests only (168)
cargo test --test cli_tests  # Integration tests only (75)
cargo test test_name         # Single test by name

Output Formats

All commands that produce output support --format:

FormatModuleDescription
tablecli/formats/table.rsHuman-readable terminal tables (default)
jsoncli/formats/json.rsMachine-readable JSON
yamlcli/formats/yaml.rsYAML output
htmlcli/formats/html.rsStandalone HTML reports

CI Pipeline

.github/workflows/ci.yml runs on every push/PR:

  1. cargo fmt --all -- --check
  2. cargo clippy --all-targets --all-features -- -D warnings
  3. cargo check
  4. cargo test --bin anvil (unit tests)
  5. cargo test --test cli_tests (integration tests)
  6. cargo build --release
  7. Verify binary runs (./anvil --version)

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Added

  • Declarative assertions for workload health validation (assertions: field in workload YAML)
  • Condition engine with 9 predicate types: command_exists, file_exists, dir_exists, env_var, path_contains, registry_value, shell, plus all_of/any_of composition
  • --assertions-only flag for anvil health to run only assertion checks
  • assertion_check toggle in workload health configuration
  • Assertion examples in anvil init --template full scaffold
  • Multi-manager workload schema: packages.brew (Homebrew) and packages.apt (APT) fields alongside existing packages.winget
  • Platform-aware validation warns when workload references an unavailable package manager
  • Inline commands: block for workload command execution with pre_install and post_install phases
  • Conditional command execution via when: field using the predicate engine
  • continue_on_error option for commands that should not block the install flow
  • Configurable workload search paths via ~/.anvil/config.yaml (user paths prepended to defaults)
  • Search precedence with conflict resolution: explicit path > user-configured > defaults; first match wins
  • anvil list --all-paths to show all discovered paths including shadowed duplicates
  • Cross-platform release builds for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64)

Changed

  • Crate renamed to anvil-cli for crates.io publishing (binary name stays anvil; install via cargo install anvil-cli)

Deprecated

  • scripts.health_check when used alongside assertions (migrate to declarative assertions; removal planned for v1.0)
  • scripts.pre_install and scripts.post_install when used alongside commands (migrate to inline commands; removal planned for v1.0)

0.5.0 - 2026-04-17

This is the first release from the new repository home at kafkade/anvil. It marks a fresh start with modernized CI/CD, updated documentation, and a clear cross-platform direction. Prior changelog entries (v0.1.0–v0.3.1) are preserved below for historical context.

Added

  • Architecture reference document for contributors (docs/ARCHITECTURE.md)
  • Automated releases from CHANGELOG.md with SHA256 checksums
  • Consolidated CI pipeline with formatting, linting, and testing in a single gate

Changed

  • Project rebranded to "Declarative Workstation Configuration Management" to reflect cross-platform direction
  • Streamlined CI from 3 separate jobs to a single Validate gate plus release build
  • Release workflow aligned with org-wide pattern (changelog-driven notes, semver pre-release detection)
  • Resolved all clippy warnings and formatting issues across the codebase

0.3.1 - 2026-01-10

Added

  • Comprehensive user documentation (USER_GUIDE.md)
  • Workload authoring guide (WORKLOAD_AUTHORING.md)
  • Troubleshooting guide (TROUBLESHOOTING.md)
  • Integration test suite for CLI commands
  • GitHub Actions CI/CD workflows
  • Shell completion generation for PowerShell, Bash, Zsh, and Fish
  • CONTRIBUTING.md with contribution guidelines

Changed

  • Improved error messages throughout CLI
  • Enhanced validation reporting with detailed messages
  • Updated README with badges and clearer instructions

Fixed

  • Various documentation typos and inconsistencies

0.3.0 - 2026-01-08

Added

  • Shell completions command for multiple shells
  • Global configuration management (config command)
  • Backup management with create, list, show, restore, clean, and verify subcommands
  • Status command to show installation state
  • HTML output format for health reports
  • --strict flag for validation command
  • Environment variable configuration in workloads
  • PATH additions support in workloads

Changed

  • Improved inheritance resolution algorithm
  • Enhanced output formatting for all commands
  • Better progress indicators during installation

0.2.0 - 2026-01-05

Added

  • Script execution support with PowerShell and CMD
  • Pre-install and post-install script hooks
  • Health check script execution
  • Elevated privilege handling for scripts
  • Configurable script timeouts
  • Template processing with Handlebars
  • File integrity verification with SHA-256 checksums
  • Automatic backup creation before file overwrites
  • Variable expansion in paths (~, ${HOME}, etc.)

Changed

  • Improved file operation error handling
  • Enhanced dry-run output with detailed plan

Fixed

  • Path expansion on Windows with backslashes
  • File permission handling on Windows

0.1.0 - 2026-01-01

Added

  • Initial release of Anvil
  • Core CLI commands: install, health, list, show, validate, init
  • Package management via winget integration
    • Install packages with version pinning
    • Custom winget arguments support
    • Package health verification
  • File operations
    • Copy files to target locations
    • Path variable expansion
  • Workload system
    • YAML-based workload definitions
    • Workload inheritance and composition
    • Circular dependency detection
  • Multiple output formats
    • Table (default, human-readable)
    • JSON (machine-readable)
    • YAML
  • Bundled workloads
    • essentials: Core development tools (VS Code, Git, Windows Terminal, Oh My Posh) and productivity utilities
    • rust-developer: Rust toolchain with cargo tools (extends essentials)
    • python-developer: Python with uv package manager (extends essentials)

Documentation

  • Initial specification document
  • Phase development prompts

Contributing

Thank you for your interest in contributing to Anvil! This document provides guidelines and instructions for contributing to the project.


Code of Conduct

This project follows the Contributor Covenant Code of Conduct. Please be respectful and constructive in all interactions.


How to Contribute

Reporting Bugs

Before reporting a bug:

  1. Check existing issues to avoid duplicates
  2. Gather relevant information:
    • Anvil version (anvil --version)
    • Windows version
    • Steps to reproduce
    • Expected vs actual behavior
    • Verbose output (anvil -vvv <command>)

Create a bug report with this template:

## Environment
- Anvil version: 
- Windows version: 
- PowerShell version: 

## Description
Brief description of the bug.

## Steps to Reproduce
1. 
2. 
3. 

## Expected Behavior
What you expected to happen.

## Actual Behavior
What actually happened.

## Verbose Output

(paste -vvv output here)


## Workload (if applicable)
```yaml
(paste workload.yaml here)

### Suggesting Features

1. Check existing issues and discussions for similar suggestions
2. Open a feature request issue with:
   - Clear description of the feature
   - Use case and motivation
   - Proposed implementation (if you have ideas)

### Submitting Code

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests (`cargo test`)
5. Run lints (`cargo clippy`)
6. Format code (`cargo fmt`)
7. Commit with a descriptive message
8. Push to your fork
9. Open a Pull Request

---

## Development Setup

### Prerequisites

- **Rust**: 1.75 or later ([rustup.rs](https://rustup.rs/))
- **Windows**: Visual Studio Build Tools (for linking)
- **Windows Package Manager (winget)**: For package operations (integration tests)
- **Linux/macOS**: Standard build toolchain (gcc/clang)

### Building

```powershell
# Clone your fork
git clone https://github.com/YOUR_USERNAME/anvil.git
cd anvil

# Build debug version
cargo build

# Build release version
cargo build --release

# Run tests
cargo test

# Run clippy lints
cargo clippy --all-targets --all-features -- -D warnings

# Format code
cargo fmt

# Run with debug output
cargo run -- -vvv list

Running Tests

# Run all tests
cargo test

# Run unit tests only
cargo test --lib

# Run integration tests only
cargo test --test '*'

# Run a specific test
cargo test test_name

# Run tests with output
cargo test -- --nocapture

Project Structure

anvil/
├── src/
│   ├── main.rs              # Entry point
│   ├── cli/                 # Command line interface
│   │   ├── mod.rs           # CLI definitions (clap)
│   │   ├── commands.rs      # Command argument structs
│   │   ├── completions.rs   # Shell completions
│   │   ├── output.rs        # Output handling
│   │   ├── progress.rs      # Progress indicators
│   │   └── formats/         # Output formatters
│   │       ├── mod.rs
│   │       ├── table.rs     # Table formatter
│   │       ├── json.rs      # JSON formatter
│   │       ├── yaml.rs      # YAML formatter
│   │       └── html.rs      # HTML report generator
│   ├── config/              # Configuration handling
│   │   ├── mod.rs
│   │   ├── schema.rs        # Workload schema validation
│   │   ├── workload.rs      # Workload parsing
│   │   ├── inheritance.rs   # Inheritance resolution
│   │   └── global.rs        # Global configuration
│   ├── operations/          # Command implementations
│   │   ├── mod.rs
│   │   ├── install.rs       # Install command
│   │   ├── health.rs        # Health check command
│   │   ├── list.rs          # List command
│   │   ├── show.rs          # Show command
│   │   ├── validate.rs      # Validate command
│   │   ├── init.rs          # Init command
│   │   ├── status.rs        # Status command
│   │   ├── backup.rs        # Backup command
│   │   └── config.rs        # Config command
│   ├── providers/           # External integrations
│   │   ├── mod.rs
│   │   ├── winget.rs        # Winget package manager
│   │   ├── filesystem.rs    # File operations
│   │   ├── script.rs        # Script execution
│   │   ├── template.rs      # Template processing
│   │   └── backup.rs        # Backup provider
│   └── state/               # State management
│       ├── mod.rs
│       ├── installation.rs  # Installation state
│       ├── cache.rs         # Cache management
│       └── files.rs         # File state tracking
├── examples/               # Example workloads
│   ├── minimal/
│   ├── rust-developer/
│   └── python-developer/
├── tests/                   # Integration tests
│   ├── cli_tests.rs
│   └── common/
│       └── mod.rs
└── docs/                    # Documentation
    └── src/
        ├── SUMMARY.md
        ├── introduction.md
        ├── user-guide.md
        ├── workload-authoring.md
        ├── troubleshooting.md
        ├── specification.md
        ├── architecture.md
        ├── changelog.md
        └── contributing.md

Key Modules

  • cli: Handles command-line parsing with clap and output formatting
  • config: Parses workload YAML files and handles inheritance
  • operations: Implements each CLI command's business logic
  • providers: Interfaces with external systems (winget, filesystem, PowerShell)
  • state: Tracks installation state and manages caching

Coding Standards

Rust Style

  • Follow standard Rust conventions and idioms
  • Use rustfmt for formatting (default settings)
  • Address all clippy warnings
  • Use thiserror for error types
  • Use anyhow for error propagation in application code

Code Organization

  • Keep functions focused and small
  • Use descriptive names for functions and variables
  • Add doc comments for public APIs
  • Use modules to organize related functionality

Error Handling

#![allow(unused)]
fn main() {
// Define custom errors with thiserror
#[derive(Debug, thiserror::Error)]
pub enum WorkloadError {
    #[error("Workload '{name}' not found")]
    NotFound { name: String },
    
    #[error("Invalid workload schema: {message}")]
    InvalidSchema { message: String },
}

// Use anyhow for propagation
pub fn load_workload(name: &str) -> anyhow::Result<Workload> {
    // ...
}
}

Documentation

#![allow(unused)]
fn main() {
/// Brief description of the function.
///
/// More detailed description if needed.
///
/// # Arguments
///
/// * `name` - The workload name to load
///
/// # Returns
///
/// The loaded workload or an error
///
/// # Errors
///
/// Returns an error if the workload is not found
///
/// # Examples
///
/// ```
/// let workload = load_workload("my-workload")?;
/// ```
pub fn load_workload(name: &str) -> Result<Workload> {
    // ...
}
}

Commit Messages

  • Use present tense ("Add feature" not "Added feature")
  • Use imperative mood ("Fix bug" not "Fixes bug")
  • Keep the first line under 72 characters
  • Reference issues when applicable

Examples:

Add shell completions for Fish

Implement Fish shell completion generation using clap_complete.
Closes #42
Fix file backup path on Windows

Handle UNC paths correctly when creating backups.
Fixes #55

Testing

Unit Tests

Add unit tests in the same file as the code being tested:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_function_name() {
        // Arrange
        let input = "test";
        
        // Act
        let result = function_under_test(input);
        
        // Assert
        assert_eq!(result, expected);
    }
}
}

Integration Tests

Add integration tests in tests/cli_tests.rs:

#![allow(unused)]
fn main() {
#[test]
fn new_command_works() {
    anvil()
        .args(["new-command", "arg"])
        .assert()
        .success()
        .stdout(predicate::str::contains("expected output"));
}
}

Test Guidelines

  • Each test should be independent
  • Use descriptive test names
  • Test both success and failure cases
  • Use tempfile::TempDir for file operations
  • Don't rely on external system state where avoidable

Submitting Changes

Pull Request Process

  1. Update documentation if needed
  2. Add tests for new features
  3. Ensure CI passes (format, lint, test, build)
  4. Write a clear PR description explaining:
    • What the change does
    • Why it's needed
    • How to test it
  5. Request review from maintainers
  6. Address feedback promptly
  7. Squash commits if requested

PR Title Format

  • feat: Add new feature
  • fix: Fix specific bug
  • docs: Update documentation
  • test: Add tests
  • refactor: Restructure code
  • chore: Update dependencies

Creating Workloads

Contributions of new bundled workloads are welcome! See the Workload Authoring guide for details.

Workload Guidelines

  1. Include meaningful health checks - Verify the workload achieves its purpose
  2. Document the workload purpose - Clear description and comments
  3. Test on clean Windows installation - Ensure it works from scratch
  4. Use inheritance for common bases (extend essentials if appropriate)
  5. Keep packages minimal - Only include what's necessary
  6. Validate before submitting - anvil validate your-workload --strict

Workload Structure

workload-name/
├── workload.yaml       # Required: workload definition
├── files/              # Optional: configuration files
└── scripts/            # Optional: setup/health scripts

Example

name: my-workload
version: "1.0.0"
description: "Brief description of what this workload provides"

extends:
  - essentials     # If applicable

packages:
  winget:
    - id: Package.ID

scripts:
  health_check:
    - path: scripts/health.ps1
      name: "Verification"
      description: "Verify installation"

License

By contributing to Anvil, you agree that your contributions will be licensed under the MIT or Apache-2.0 license, at the user's choice.


Questions?

Thank you for contributing to Anvil!