Skip to content

Packages

Packages are declared in configuration and installed by the native package manager.

packages:
  brew: [ripgrep, fd, jq]
  pacman: [keyd, base-devel]
  aur: [statemate-bin]
  common: [git, curl]

Supported managers

Key Manager Detected by
brew Homebrew brew on $PATH
pacman Arch's pacman pacman on $PATH
aur An AUR helper (yay or paru) the helper on $PATH
common Whichever of brew/pacman is present —

Homebrew casks go under brew: too; there is no separate cask: key.

A manager that is not available is ignored entirely, so one config can serve both a macOS and an Arch machine.

common

common resolves to the primary manager: brew if present, otherwise pacman. It never resolves to the AUR. Use it for packages named identically everywhere:

packages:
  common: [git, ripgrep, fd]

If the same package is also listed under a specific manager, the two entries merge rather than duplicating.

Homebrew taps

A formula from a third-party tap can be declared either by its fully-qualified name or bare, and both are recognised as installed:

packages:
  brew:
    - jamf/internal-tap/hermes    # fully qualified
    - hermes                      # equivalent for an installed formula

Prefer the qualified form: brew install needs it to find a formula that is not already tapped, whereas the bare name only works once the tap is added.

Statemate accepts either because Homebrew itself is inconsistent — brew list --formula reports a tap formula under its bare name while brew leaves reports it fully qualified. One consequence is that two taps providing the same formula name cannot be told apart when comparing bare names.

statemate does not run brew tap itself, but brew install owner/tap/formula taps automatically, which is another reason to prefer the qualified form.

AUR helper

When aur_helper is unset, statemate uses yay if it is on $PATH, otherwise paru. Set it to choose explicitly:

aur_helper: paru

Without either helper, aur: packages are ignored, like any unavailable manager.

AUR packages are queried separately from native ones, so an AUR package is not reported as an unexpected extra under pacman.

Where packages can be declared

All of these are merged, and a package may appear in several:

# mate.yaml — every machine
packages:
  brew: [ripgrep]

profiles:
  work:
    packages:                 # only under the work profile
      brew: [awscli]

include:
  - packages.yaml             # packages: and variables: only

Profile packages are inherited along extends, like sources and variables: a profile that extends base gets base's packages too. A profile can also have its own include:.

# nvim/.mate.yaml — only when this source is active
packages:
  common: [neovim]

mate packages status shows where each package was declared, which is the quickest way to find out why something is on the list: config for mate.yaml and its includes, profile:<name> for a profile, or the source's name.

A packages: key in your local config replaces the repository's top-level packages rather than adding to them.

Versions

statemate does not pin versions. A package name is passed to the manager exactly as written, so Homebrew's versioned formulae work like any other package:

packages:
  brew: [node@20, python@3.12]

Commands

mate packages status           # every declared package: ✓ installed, + missing
mate packages status --all     # also list installed packages not in config
mate packages status -v        # include package descriptions
mate packages apply            # install what is missing
mate packages apply --prune    # also remove packages not in config
mate packages apply -y         # without the confirmation prompt

mate packages apply asks once per manager before installing.

mate apply also prompts, after writing files, once per manager:

Missing pacman packages: keyd, tlp
Install? [y/N]

--force answers yes. --dry-run lists the packages without prompting. Without a terminal to prompt on, installs are skipped with a warning.

mate apply <path> skips packages entirely, and mate apply --source <name> only considers that source's packages. mate status reports missing packages under "Missing packages".

The commands statemate runs:

Manager Install Remove (--prune)
Homebrew brew install … brew uninstall …
pacman sudo pacman -S --noconfirm … sudo pacman -R --noconfirm …
AUR <helper> -S --noconfirm … <helper> -R --noconfirm …

All of a manager's missing packages go into one command, so one bad name can fail the whole batch.

--all and extras

Without --all, mate packages status reports only what is missing. Listing extras — installed packages that no source declares — means asking the manager for every explicitly-installed package, which takes about a second with brew. That cost is only paid when you ask for it:

Use --all to also show packages not in config.

For the same reason mate status and mate apply never compute extras.

-v and <unknown>

-v adds a DESCRIPTION column. Two kinds of empty look different on purpose:

 ✓  font-hack-nerd-font    brew   macos
 +  github-cli             brew   git     <unknown>

An empty description means the package manager knows the package but publishes no description for it (common for font casks). <unknown> means the manager matched no such package at all, which usually points at a mistake in your config — a typo (github-cli where Homebrew calls it gh), a name that only exists on another platform (man, sudo in a common: list on macOS), or a package that has since been renamed or removed. Such a package can never be installed. It also fails the install of every other package in the same batch until the name is corrected.

<unknown> is only reported for Homebrew; pacman and the AUR leave the description empty.

A Homebrew alias is described under the formula it is installed as, so kubectl shows the description of kubernetes-cli. An alias of a formula that is not installed is still reported as <unknown>, because the local name lists brew publishes contain no aliases — declaring the canonical name avoids the ambiguity.

--prune

--prune uninstalls anything not declared in your configuration. Since "not declared" includes packages you installed deliberately but never wrote down, review mate packages status --all first.

What counts as installed

A declared package counts as installed if it is present at all, including as a dependency of something else.

Finding extras, for --all and --prune, is stricter. Only packages you installed explicitly count (brew leaves --installed-on-request plus every cask, pacman -Qen, and <helper> -Qmtt for the AUR), so dependencies are never offered for removal.

Virtual packages and provides are resolved, so declaring man is satisfied by man-db.

Homebrew aliases count as installed: declaring kubectl is satisfied by kubernetes-cli, and az by azure-cli. Only the canonical name appears in brew list, so the alias is resolved through the opt/ link Homebrew creates for it.