Deprecated — pkgm has been merged into dotm. No further updates will be made. Use dotm instead.
Declarative package and service manager. Repository is the source of truth — no local state beyond a manifest for diffing. Zero hardcoded package managers or init systems.
cd ~/projects/dotfiles # where pkgm.toml lives
pkgm init # answer prompts (if any)
pkgm apply # install desired, remove obsolete
pkgm apply --dry-run # preview changes
pkgm status # current vs desired state
pkgm reset laptop # reset specific prompt
pkgm reset --all # reset all cached prompts
pkgm version # print versionCreate pkgm.toml in your dotfiles repository:
# ─── Managers ───
[managers.pacman]
check = "pacman -Q {{.Name}}"
install = "sudo pacman -S --needed {{.Name}}"
remove = "sudo pacman -Rns {{.Name}}"
[managers.aur]
check = "pacman -Q {{.Name}}"
install = "aur sync --no-view -n {{.Name}} && sudo pacman -S --needed {{.Name}}"
remove = "sudo pacman -Rns {{.Name}}"
[managers.systemd]
check = "systemctl is-enabled {{.Name}}"
enable = "systemctl enable {{.Name}}"
disable = "systemctl disable {{.Name}}"
[managers.systemd-user]
check = "systemctl --user is-enabled {{.Name}}"
enable = "systemctl --user enable {{.Name}}"
disable = "systemctl --user disable {{.Name}}"
# ─── Packages ───
[pacman]
packages = [
"hyprland", "neovim", "zsh", "pipewire", "pipewire-pulse",
]
[aur]
packages = ["kopia-bin", "coolercontrol-bin"]
# ─── Services ───
[systemd]
services = [
"firewalld",
"systemd-oomd",
]
[systemd-user]
services = [
"hypridle",
"waybar",
"mpd",
]pkgm applyreadspkgm.tomland loads the previous manifest- For each package: runs
check→ if not installed, runsinstall - For each package in manifest but not in config: if still installed, runs
remove - For each service: runs
check→ if not enabled, runsenable - For each service in manifest but not in config: if still enabled, runs
disable - Saves new manifest to
~/.local/state/pkgm/<hash>.toml(filename is first 8 bytes of SHA-256 hash of config directory path)
Add a section to [managers] — no code changes needed:
# Package manager
[managers.pip]
check = "pip list 2>/dev/null | grep -q '{{.Name}}'"
install = "pip install --user {{.Name}}"
remove = "pip uninstall -y {{.Name}}"
# Service manager (openrc example)
[managers.openrc]
check = "rc-status | grep -q '{{.Name}}'"
enable = "rc-update add {{.Name}} default"
disable = "rc-update del {{.Name}}"
# Then use it:
[openrc]
services = ["sshd", "NetworkManager"]Package and service names can contain Go template expressions. If a name renders to an empty string, the entry is skipped:
[pacman]
packages = [
"git",
"{{ if .nvidia }}nvidia-utils{{ end }}",
"{{ if .tablet }}opentabletdriver{{ end }}",
]All prompt values are available as template variables:
[prompts]
nvidia = { type = "bool", question = "Use NVIDIA config?" }
tablet = { type = "bool", question = "Do you use a tablet?" }
[pacman]
packages = [
"base",
"{{ if .nvidia }}nvidia-open{{ end }}",
]Interactive prompts store user preferences in state. Run pkgm init to
answer them.
[prompts]
laptop = { type = "bool", question = "Is this a laptop?" }
git_email = { type = "string", question = "Git email address" }Types: bool (y/n), string (free text). Values are cached in
~/.local/state/pkgm/ and reused across runs.
git clone https://github.com/fkzys/pkgm.git
cd pkgm
sudo make installAGPL-3.0-or-later