| name | dotfiles-add-cookbook |
| description | Add a new mitamae cookbook for a tool, package, LSP server, or application in this dotfiles repository. Use this skill whenever the user says things like "add cookbook for X", "install X via mitamae", "add X to dotfiles", "add LSP for X", "provision X", "set up X in my environment". Also trigger when the user just names a tool and implicitly expects it to be added to the repo.
|
Add a New Mitamae Cookbook
Before you write anything
Step 1: Check the mitamae docs if unsure about any resource behavior.
The official README is at: https://raw.githubusercontent.com/itamae-kitchen/mitamae/refs/heads/master/README.md
Fetch it with WebFetch when you're uncertain about resource semantics, attribute names, or execution order.
Step 2: Research the tool's recommended installation method.
Use WebSearch to find:
- The tool's official installation docs
- Which package managers provide it (brew, apt, cargo, pip, npm, etc.)
- Whether OS-native packages are up to date vs stale (e.g.,
apt install neovim installs a very old version — avoid it)
- The latest stable version if you'll pin a version number
Step 3: Choose the installation method using this priority order (highest to lowest):
| Priority | Method | When to use |
|---|
| 1st | package / cross_platform_package | Available via brew (macOS) or apt (Linux) AND the version is reasonably current |
| 2nd | cargo_package | A Rust binary; cargo gives the latest version |
| 3rd | uv_tool_package | A Python CLI tool |
| 4th | npm_global_package | A Node.js CLI tool |
| 5th | execute + official installer script | When package managers give stale versions or the tool isn't packaged |
| 6th | snap (Linux only) | Tool is officially distributed via Snap Store; macOS uses brew/cask |
| Last | sdkman or other version managers | Only when absolutely necessary |
If the apt/brew package is significantly outdated, note this explicitly and fall back to the next method.
Repository structure
mitamae/
├── lib/custom_resources.rb # Shared resource helpers (read this for idioms)
├── cookbooks/<name>/
│ ├── default.rb # Installation logic (required)
│ ├── files/ # Dotfiles to symlink into $HOME
│ └── templates/ # ERB templates for generated config files
└── roles/<variant>/default.rb # Include cookbooks here
Roles follow an inheritance chain: plum → bamboo → pine
plum: bare essentials (shell, editor, base tools)
bamboo: daily dev tools (git, LSPs, AI assistants)
pine: specialized / hardware / niche tools
Custom resource idioms
These helpers are defined in mitamae/lib/custom_resources.rb. Always prefer them over raw resources.
cross_platform_package — system package manager (preferred)
cross_platform_package 'ripgrep'
cross_platform_package 'sqlite',
darwin_name: 'sqlite',
debian_name: 'sqlite3'
Debian installs run as root automatically. Darwin uses brew.
apt_repository — third-party APT repositories (Debian/Ubuntu)
Use inside the Debian/Ubuntu branch when a tool ships via its own APT repo
instead of the base distro. It fetches the signing key, normalizes it to binary
with gpg --dearmor (works for both armored and already-binary keys), stores it
at /etc/apt/keyrings/<name>.gpg, writes /etc/apt/sources.list.d/<name>.list
(scoped to that key via signed-by and pinned to the host arch), and runs
apt-get update. curl, ca-certificates, and gnupg are installed
automatically. Always prefer this over hand-rolling the key/sources/update dance
in a raw execute — apt's signed-by is extension-sensitive, and getting the
key format wrong silently breaks verification.
apt_repository 'google-chrome' do
key_url 'https://dl.google.com/linux/linux_signing_key.pub'
repo 'https://dl.google.com/linux/chrome/deb/ stable main'
end
codename = '$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")'
apt_repository 'docker' do
key_url "https://download.docker.com/linux/#{node[:platform]}/gpg"
repo "https://download.docker.com/linux/#{node[:platform]} #{codename} stable"
end
repo is everything after the [options] block of the sources line. Pair it
with an explicit package '<name>' do user 'root' end for the actual install
and an unsupported_platform! fallback for other platforms.
brew_cask — macOS GUI applications
Only available on macOS. Use for .app bundles (not CLI tools). Pair it with
an explicit unsupported_platform! fallback so the cookbook fails loudly if it
is ever reached on a non-macOS host (see "Unsupported platforms" below).
if node[:platform] == 'darwin'
brew_cask 'wezterm'
else
unsupported_platform! node[:platform]
end
unsupported_platform! — fail loudly on platforms you don't support
Always terminate platform branches with this helper instead of silently
skipping. See "Unsupported platforms" below for the full policy.
unsupported_platform! node[:platform]
cargo_package — Rust binaries
Automatically installs rust as a dependency.
cargo_package 'eza'
cargo_package 'bat', bin_name: 'bat'
uv_tool_package — Python CLI tools
Automatically installs uv as a dependency. Never use pip.
uv_tool_package 'ruff'
uv_tool_package 'python-lsp-server', bin_name: 'pylsp'
npm_global_package — Node.js CLI tools
Automatically installs nodenv as a dependency and rehashes shims.
npm_global_package 'prettier'
npm_global_package 'typescript-language-server', version: '4.3.3'
npm_global_package 'opencode-ai', bin_name: 'opencode'
dotfile — symlink a file into $HOME
dotfile '.tool-config' do
cookbook_dir __dir__
end
File must live at files/.tool-config within the cookbook directory.
dotconfig — symlink a directory into ~/.config
dotconfig 'mytool'
Source must live at .config/mytool/ in the repo root.
snap — Linux snap packages (macOS: use brew/cask instead)
Install snapd first, then snap install. Always specify --classic when the tool requires it (check the Snap Store page).
if node[:platform] == 'darwin'
brew_cask 'mytool'
elsif %w[ubuntu debian].include?(node[:platform])
package 'snapd' do
user 'root'
end
execute 'Install mytool via snap' do
command 'snap install mytool --classic'
user 'root'
not_if 'snap list | grep -q mytool'
end
else
unsupported_platform! node[:platform]
end
execute — custom install commands
Use when none of the helpers above apply. Always add a not_if guard.
execute 'install mytool' do
command 'curl -fsSL https://example.com/install.sh | sh'
user 'root'
not_if 'command -v mytool'
end
Platform and architecture patterns
Platform detection
node[:platform] == 'darwin'
%w[ubuntu debian].include?(node[:platform])
Unsupported platforms (required)
Every cookbook must handle each platform it supports explicitly and fail
loudly on the rest by calling unsupported_platform! node[:platform]. Never
leave a bare if/elsif without an else, and never write
... if node[:platform] == 'darwin' as a silent guard — both hide
role/platform misconfigurations behind a no-op.
if node[:platform] == 'darwin'
package 'mytool'
elsif %w[ubuntu debian].include?(node[:platform])
else
unsupported_platform! node[:platform]
end
Cookbooks built solely on cross_platform_package, cargo_package,
uv_tool_package, or npm_global_package already embed this policy and need no
extra branch.
Architecture
node[:os_arch] is already normalized by custom_resources.rb:
aarch64 / arm64 → 'arm64'
x86_64 / amd64 → 'x86_64'
Use it in download URLs:
url = "https://github.com/example/releases/download/v#{version}/tool-linux-#{node[:os_arch]}.tar.gz"
Multi-platform example with arch-specific binary download
mytool_version = '1.2.3'
if node[:platform] == 'darwin'
package 'mytool'
elsif %w[ubuntu debian].include?(node[:platform])
execute 'install mytool' do
command <<~EOC
curl -fsSLO "https://github.com/example/mytool/releases/download/v#{mytool_version}/mytool-linux-#{node[:os_arch]}.tar.gz"
tar xzf "mytool-linux-#{node[:os_arch]}.tar.gz" mytool
install mytool /usr/local/bin/mytool
rm -f mytool "mytool-linux-#{node[:os_arch]}.tar.gz"
EOC
user 'root'
not_if "mytool --version 2>/dev/null | grep -q '#{mytool_version}'"
end
else
unsupported_platform! node[:platform]
end
When pinning a version: always search the web to confirm the latest release before hardcoding it.
mruby limitations (important!)
mitamae runs on mruby, not full Ruby. Common gotchas:
| Don't use | Use instead |
|---|
Dir.home | ENV['HOME'] |
__dir__ (inconsistent) | File.dirname(__FILE__) |
require (most stdlib) | Not available |
| String interpolation in heredoc delimiter | Use plain delimiter like EOC |
Goss assertion
Every cookbook must have a goss.yaml for health-check validation. Prefer command checks over file checks — command checks work identically on macOS and Linux regardless of installation path differences.
command:
mytool --version:
exit-status: 0
timeout: 5000
Only use file checks for non-executable artifacts (symlinked dotfiles, config directories, etc.) that have no runnable command.
The goss validate step exports these directories to PATH automatically, so command checks will find binaries installed by any method:
~/.cargo/bin (cargo_package)
~/.local/bin (uv_tool_package)
~/.nodenv/shims and ~/.nodenv/bin (npm_global_package)
After creating cookbooks/<name>/goss.yaml, register it in the role's goss.yaml:
gossfile:
../../cookbooks/<name>/goss.yaml: {}
LSP server registration
When the cookbook installs an LSP server binary, also register it in .config/nvim/lua/lsp/languages.lua.
Step 1: Look up the server name in the nvim-lspconfig docs:
https://raw.githubusercontent.com/neovim/nvim-lspconfig/refs/heads/master/doc/configs.md
Step 2: Add the minimal registration at the bottom of the file:
vim.lsp.enable('mytool_lsp')
Step 3: If the LSP conflicts with a formatter (e.g., ts_ls + biome), disable its formatting capability:
vim.lsp.config('mytool_lsp', {
on_attach = disable_formatting,
})
vim.lsp.enable('mytool_lsp')
The disable_formatting helper is already defined at the top of the file.
Checklist before finishing
Adding to a role
Edit mitamae/roles/<variant>/default.rb and add:
include_recipe '../../cookbooks/<name>'
Choose the most restrictive role that makes sense:
plum — everyone needs this (rare)
bamboo — standard dev tool
pine — niche / hardware / heavy tool