Skip to content

plan & apply

Resolves facts, resolves the config for this machine, diffs against what is recorded and what is on disk, and prints the result. It changes nothing.

Bedouin will make the following changes:
+ manager brew not installed
~ package zellij 0.39.2 -> 0.40.1 cargo
- package ripgrep was: apt, owner: bedouin
Plan: 1 to add, 1 to change, 1 to remove.

Exit codes are made for scripting: 0 nothing pending, 2 changes pending, 1 error. A CI drift check just reads the status.

-v annotates each conditional value with the arm that won, and lists what only: pruned:

+ package neovim latest apt
from = noble

That is the one visible trace of arm selection, and it is what you reach for when a config resolves differently than you expected.

Shows the plan, asks, then executes. -y skips the question.

It is not transactional, and does not pretend to be — rolling back a package manager is not something anything can do honestly. Instead:

  • A step records its intent before it begins and marks it complete after. A run interrupted between installing and recording leaves an honest record, not a package that looks like it was always there.
  • On failure it stops, prints the failed step, its output, and what it did not attempt. A half-configured machine reporting success is worse than one reporting where it broke.
  • Re-running resumes, because plan re-diffs. There is no separate resume.

Stopping is right when the failure is a surprise. When it is not — a package from an apt repository this machine has not been given yet — halting the other fifty steps helps nobody:

Terminal window
bedouin apply --skip 1password-cli,caddy
bedouin apply --skip package/1password-cli # the full id works too

Takes the id a failure prints (package/jq) or just the name (jq). Skipped steps are named in the report, never silently dropped: a run that quietly did less than the plan promised is the thing this tool exists not to do. They stay in the next plan, because nothing about the config changed.

Each step gets a heading, its command output is dimmed beneath it, and the plan colours its sigils — green to add, amber to change, red to remove:

:: [7/57] package/ripgrep
Setting up ripgrep (14.1.0-1) ...
:: [8/57] language/rust
info: syncing channel updates for stable

Colour is decided once, from whether stdout is a terminal, NO_COLOR, and TERM. A pipe, a CI log and a redirect to a file all get plain text without being asked, which is also what makes the output safe to paste into a bug report.

Terminal window
bedouin plan -o plan.json
# read it, commit it, hand it to someone
bedouin apply -f plan.json

The artifact freezes the resolved facts including the environment variables your config reads — only those, at mode 0600. Without that, a plan reviewed in one terminal could mean something else in another, since the environment is process-scoped and otherwise unrecorded.

apply -f re-checks the machine state and facts and refuses a stale plan. It does not re-check the environment: that is the thing the artifact exists to carry forward.