Skip to content

ADR-004: Install configuration is declarative package sets with an explicit shell escape hatch

Status: accepted

Context

over apply --install needs to express "these packages should be present" across several package managers (archlinux, apt, brew, winget, cargo, python, node). A fully declarative model — packages only, no scripts — is safer and easier to audit and dedupe across uses. But it cannot express manager bootstrapping: adding a Homebrew tap that needs authentication, or a step that must run before or after a manager without being itself a package.

Decision

Packages are typed, serde-flexible values (flat string or full object, src/actions/install.rs), collected into a BTreeSet and unioned across uses by collect_packages — pure data, sorted, deduplicated, no shell involved. But pre/post fields exist at every level (global, per-platform, per-manager) and run as arbitrary shell via run_cmd(ctx, "sh", &["-c", script]).

Manager selection is likewise a fixed rule, not user-authored: decide_ linux_managers picks one manager by hard-coded precedence (archlinux > brew on Arch, apt > brew on Debian/Ubuntu) when only top-level configs are set, but runs every configured manager once a platform-specific override block exists.

Consequences

The common case — "install these packages" — stays declarative, auditable, and dedupable; the escape hatch covers what a closed package-list model cannot. The cost: --install is not sandboxed and not idempotency-checked by the framework — a pre/post script re-runs on every apply, and its safety is entirely on the author. The platform-config-present-or-absent branching in manager selection is also a subtle rule: adding a platform block changes whether one manager runs or all of them do, which is easy to get wrong without reading the source.