Cross-platform desktop setup using Ansible and Mise.
Targets macOS, Ubuntu, and Windows (via WSL2). Idempotent — run at any time to install missing tools or apply updates.
| Layer | Tool | Role |
|---|---|---|
| Orchestrator | Ansible | Idempotent install, config, and dotfile deployment |
| Runtimes | Mise | Node, Python, Rust version management |
| Package managers | Homebrew / apt+snap+flatpak / WinGet | Platform-native package installation |
Ansible runs locally on macOS and Ubuntu (connection: local).
On Windows, it runs from WSL2 and connects to the Windows host over WinRM using the WSL2 gateway address.
Run once on a fresh machine to install the prerequisites Ansible and Just on all platforms, and Git for Windows:
# macOS
bash bootstrap/bootstrap_macos.sh
# Ubuntu / WSL2
bash bootstrap/bootstrap_ubuntu.sh
# Windows — run first in PowerShell as Administrator
.\bootstrap\bootstrap_windows.ps1just installjust upgradePreview exactly what just install would change without touching the machine:
just dry-runRuns the playbook with --check --diff: shows which tasks would make changes and diffs any file content that would be modified.
The verification step is skipped because commands do not actually execute in check mode.
Each role is tagged so you can re-run it without running the full playbook:
| Tag | Role |
|---|---|
bootstrap |
Package manager setup |
runtimes |
Mise, Node, Python, Rust, global packages |
tools |
CLI tools |
apps |
GUI applications |
dotfiles |
Config files and scripts |
git |
Global git configuration |
os_config |
VS Code extensions, macOS system defaults |
just install-tags dotfiles # redeploy config files only
just install-tags git # reapply git config (e.g. after editing local.yml)
just install-tags dotfiles git # multiple roles at once
just dry-run-tags dotfiles # preview config file changes only
just dry-run-tags git # preview git config changes onlyWindows packages are managed from WSL2 via Ansible + WinRM. The flow is:
- Run
bootstrap\bootstrap_windows.ps1in PowerShell as Administrator — installs Git, configures WinRM, sets up WSL2. - In WSL2, clone this repo and run
bash bootstrap/bootstrap_ubuntu.sh(installs Ansible). - Run the install command above with the
windows.ymlinventory.
The Ansible playbook connects back to the Windows host over WinRM and installs everything via WinGet.
.
├── ansible/
│ ├── inventory/
│ │ ├── group_vars/all.yml # Cross-platform vars (git config, cargo/pip packages)
│ │ ├── group_vars/local.yml.example # Template for gitignored personal overrides
│ │ ├── localhost.yml # macOS / Ubuntu target
│ │ └── windows.yml # Windows target (WinRM via WSL2 gateway)
│ ├── playbooks/
│ │ ├── install.yml # Main install playbook (runs verify at end)
│ │ ├── upgrade.yml # Upgrade all packages
│ │ └── verify.yml # Verify tools are installed
│ ├── requirements.yml # Ansible Galaxy collections
│ └── roles/
│ ├── bootstrap/ # Package manager setup (Homebrew, apt repos, WinGet)
│ ├── runtimes/ # Node, Python, Rust via Mise
│ ├── tools/ # CLI tools
│ ├── apps/ # GUI applications
│ ├── dotfiles/ # Config file and script deployment
│ ├── git/ # Git global configuration
│ └── os_config/ # OS-level config (VS Code extensions, Docker group, etc.)
├── bootstrap/
│ ├── bootstrap_macos.sh # Installs Homebrew, Ansible
│ ├── bootstrap_ubuntu.sh # Installs pipx, Ansible
│ └── bootstrap_windows.ps1 # Installs Git, configures WinRM, sets up WSL2
├── config/
│ ├── starship.toml # Starship prompt config
│ └── bottom.toml # bottom system monitor config
├── docs/
│ ├── principles.md # Architecture principles
│ └── adr/ # Architecture Decision Records
├── scripts/
│ ├── ff # Fuzzy-find script (macOS, Ubuntu)
│ └── ll # Colourful directory listing (macOS, Ubuntu)
├── .mise.toml # Runtime version pins (Node, Python, Rust)
└── Justfile # Convenience wrappers around ansible-playbookall means macOS, Ubuntu, and Windows.
| Runtime | Platforms | Manager |
|---|---|---|
| Node.js | all | Mise (macOS/Ubuntu), WinGet (Windows) |
| Python | all | Mise (macOS/Ubuntu), WinGet (Windows) |
| Rust | all | Mise (macOS/Ubuntu), WinGet/rustup (Windows) |
| .NET | all | Homebrew (macOS), apt (Ubuntu), WinGet (Windows) |
| Docker | all | OrbStack (macOS), apt (Ubuntu), Docker Desktop via WinGet (Windows) |
| PowerShell | Ubuntu, Windows | apt (Ubuntu), WinGet (Windows) |
| Tool | Platforms | Description |
|---|---|---|
| asciinema | macOS, Ubuntu | Record and share terminal sessions |
| bat | all | cat clone with syntax highlighting |
| bottom | all | Terminal system monitor |
| delta | all | Syntax-highlighted diffs; configured as git pager |
| eza | all | Modern ls replacement |
| fd | all | Fast and user-friendly find alternative |
| fzf | all | Command-line fuzzy finder |
| gh | all | GitHub CLI |
| Git LFS | all | Git Large File Storage |
| jq | all | sed for JSON |
| just | all | Task runner |
| lazygit | all | TUI for git |
| less | all | Terminal pager |
| pandoc | all | Universal markup converter |
| pnpm | all | Fast JavaScript package manager |
| ripgrep | all | Fast regex search |
| ripgrep-all | all | ripgrep across PDFs, Office docs, etc. |
| shellcheck | all | Shell script linter |
| Starship | all | Cross-shell prompt |
| UPX | all | Executable packer |
| zoxide | all | Smarter cd that learns your habits |
| zsh-autocomplete | macOS | Real-time tab completion for Zsh |
| zsh-autosuggestions | macOS | Fish-style history suggestions for Zsh |
| NuGet | all | .NET package manager |
| PuTTY | Windows | SSH client |
| VS Build Tools | Windows | MSVC compiler toolchain |
| Docker (apt) | Ubuntu | Container runtime |
| Flatpak | Ubuntu | Application distribution |
| ffmpeg | all | Media processing |
| App | Platforms | Description |
|---|---|---|
| VS Code | all | Code editor |
| draw.io | all | Diagramming |
| Obsidian | all | Note-taking |
| Spotify | all | Music |
| Telegram | all | Messaging |
| ghostty | macOS, Ubuntu | Fast, native terminal emulator |
| OrbStack | macOS | Container and VM runtime (Docker Desktop replacement) |
| Raindrop.io | macOS, Windows | Bookmark manager |
| 7-Zip | Windows | File archiver |
| Caesium | Ubuntu, Windows | Image compressor |
| DBeaver | all | Universal database tool |
| Amberol | Ubuntu | Music player |
| Firefox | all | Web browser |
Installed on all platforms via their respective package managers:
| Type | Packages |
|---|---|
| Cargo | cargo-modules, cargo-outdated, cargo-update, diskonaut, wasm-pack |
| pip | ansible-core, ansible-lint, checkov, ipykernel, pre-commit, setuptools |
| pnpm | cspell |
| VS Code | GitLens, EditorConfig, Markdownlint, Night Owl theme, VS Code Icons |
Deployed to ~/.local/bin on macOS and Ubuntu:
| Command | Description |
|---|---|
ff <term> |
Fuzzy find files matching a search term, previewed with bat |
ll [path] |
List files with eza (long format, git-aware, human-readable) |
Machine-specific settings that should not be committed live in a gitignored file:
cp ansible/inventory/group_vars/local.yml.example ansible/inventory/group_vars/local.ymlEdit local.yml and fill in your details:
git_user_name: "Your Name"
git_user_email: "you@example.com"Ansible picks this file up automatically. If it is absent the identity tasks are skipped — nothing breaks.
The .example file documents the available options.
- docs/principles.md — Architecture principles guiding tool and approach choices
- docs/adr/ — Architecture Decision Records
Test the code in this repo with:
just pre-commit
just ansible-lint
just spellcheck