A Day With Loadout - josemiguelo/loadout GitHub Wiki
A day with loadout
What living with declared loadouts actually looks like โ a composite day across a small fleet (an Omarchy laptop, a work Mac), using the author's live repo as the setting.
Morning: where does this machine stand?
Bare loadout on a terminal opens the home screen โ the question answered
in five rows (four in a repo without dotfiles):
loadout 2.0.0 โ t2-macbook-omarchy โ linux/omarchy ยท x86_64
! programs 59 installed ยท 1 missing โ install what's missing
! scripts 44 done ยท 1 pending run what's pending
! configs 16 applied ยท 1 drifted apply what's drifted
ยท remote โ review them
! fleet 13 program(s) drifted across 2 machines compare the fleet
โโ/jk move ยท [ ] jump to next list ยท l/โ open ยท enter act on this line
r re-check ยท S sync ยท U upgrade loadout ยท C set up this machine ยท t theme ยท q quit
It opens on the last observed state, then re-checks the machine (like
status, a few seconds) and asks the remotes (like outdated) โ both land
as they finish, the row spinning until its answer is in. l opens a row's
detail in place (the missing programs, the scripts picker, the configs
picker, the outdated table, the fleet diff);
h closes it; enter acts โ it never opens or closes anything, so it
can't surprise you: inside a picker (missing programs, maintenance
scripts, drifted configs, outdated mechanisms) it asks first, then runs what you ticked
in your terminal and brings the screen back as you left it. With nothing
to open (remotes unreachable, fleet in sync) it says so and stays.
The screen takes the whole window, like any full-screen app, and so do the
commands it runs. When a command finishes, one key decides: Enter goes
back to the screen, v pages through everything the command printed
(from its first line, even what scrolled off), q quits. Quitting gives
your terminal back exactly as it was before you opened loadout.
โโ/jk walk the whole screen, open lists included: keep going up from a
table's first row and you land on its own row, then on the one above it,
with the table still open behind you. Open as many as you like โ a row
with its list showing is marked โพ, and only h or esc on the one
you're in closes it โ esc never leaves the screen, q does. Close a
long table to read something else and l puts you back on the row you
were on. pgup/pgdn move a page at a
time, and when there is more than fits, the footer says where you are
(1-24 of 57).
[ and ] go over the rows instead of through them, landing on the
next thing that heads a list: inside the remote table that's the next
group โ brew, then mise-tools, then antidote-plugins โ and once
its groups run out, the row below.
The capitals are machine-wide verbs that also run in your terminal: S
sync, C set up this machine, U self-upgrade (after U, run loadout
again to open the new version). Every row is a command's answer and every
key runs a command, so the rest of this page is those commands.
t flips between the light and dark palettes when loadout reads your
terminal wrong. Some terminals โ a tmux popup, for one โ never tell
anyone their background colour, and there loadout would get it wrong
every time. Tell it once instead:
export LOADOUT_THEME=light # or dark
The status behind the programs and scripts rows:
$ loadout status
machine t2-macbook-omarchy โ linux/omarchy โ x86_64
PROGRAM STATUS VERSION
โ git installed 2.55.0
โ kitty installed 0.48.2
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ workmux missing - โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
SCRIPT STATUS
โ dotfiles-push done
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ dotfiles-pull pending โ
โ missing: dotfiles source is 2 commit(s) behind origin โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โ setup-git done
CONFIG STATUS
โ kitty applied
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ zsh drifted โ
โ .config/zsh/.zshrc โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
State written to โฆ/state/t2-macbook-omarchy.json
Rows that aren't settled are boxed, amber for "not yet", red for "missing"
โ consecutive ones share a box, and a failing check's explanation rides
inside the box with the row it explains. diff does the same for drift
across the fleet.
The CONFIG table is your dotfiles, when the repo holds them (see
Repo layout): a config is drifted when
loadout apply would change one of its files, and the box lists which.
A third verdict exists for the case where loadout could not ask at all. If
the tool a check goes through isn't there โ brew missing from this
shell's PATH, say โ those programs are not checked, never "missing",
and one line after the table says why:
โ brew is not on PATH โ 22 program(s) not checked (atuin, bat, fd, โฆ)
The difference matters: "missing" would offer to install 22 things that are already installed.
One table answers "is this machine carrying its declared loadout?" โ every
program's check re-asked (concurrently, a few seconds), every script's check
too, with failing checks explaining what exactly is off. Nothing here
touched the network except the checks you explicitly wrote to (the
dotfiles-pull fetch is throttled to once per 6h and silent offline). The
state file is written as a side effect โ that's the machine publishing its
reality.
Two kinds of drift just surfaced: a program this machine should carry but
doesn't (workmux was added to the fleet's loadout yesterday, on the other
machine), and dotfiles edited elsewhere that haven't landed here.
Converge it
The missing program โ l on the programs row lists it with what would
install it, ticked; enter plans it (dependencies first, like install)
and, on your yes, installs it in your terminal:
โ programs 59 installed ยท 1 missing โพ install what's missing
โฏ [x] โ workmux [brew] brew install raine/workmux/workmux
From a shell, the same act is the targeted installer:
$ loadout install workmux
Plan for t2-macbook-omarchy:
= tmux 3.7
+ workmux [brew] brew install raine/workmux/workmux
Proceed? [y/N] y
The drifted scripts โ the home screen's scripts row. l opens it as a
picker over this machine's maintenance loadout (setup-only bootstraps never
appear), each script with the verdict the last observation wrote down and
the unfinished ones already ticked:
! scripts 44 done ยท 1 pending โพ run what's pending
[ ] โ setup-git done
โฏ [x] ! dotfiles-pull pending
[ ] โ dotfiles-push done
space ticks or unticks the focused script โ a done one too, which is how
you force a re-run โ a ticks them all, u unticks them all, and enter
asks before running them in manifest order (dotfiles-push is declared
after dotfiles-pull, so ticking both pulls, then pushes):
โญโ run this? โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ These scripts will run, in order: โ
โ โ
โ dotfiles-pull Pull the dotfiles repo โ
โ โ
โ Run Cancel โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โ/โ pick a button and enter presses it; y runs, n or esc
cancels. Run hands your terminal to loadout run dotfiles-pull --force:
โโ loadout run dotfiles-pull --force โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ran dotfiles-pull (exit 0)
State updated.
! still not done: dotfiles-pull (missing: dotfiles source is 1 commit(s) behind origin)
โโ โ done โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
enter returns to loadout ยท v views the full output ยท q quits
The check decides, not the exit code: a script that still fails its check is named, with what the check printed. Enter brings the screen back with the row updated.
Then publish, so the fleet sees this machine is whole again:
$ loadout sync # pull --rebase โ apply configs โ refresh state โ commit ONLY state/<me>.json โ push
sync is cron-safe: when nothing changed, nothing is committed. A config
file you edited on this machine stops it before anything is written, since
only you know which version wins:
error: 1 config file(s) edited on this machine: .config/zsh/.zshrc โ keep your edits with `chezmoi re-add`, or replace them with `loadout apply --force`
Both converge commands take --all when you want the whole membership
rather than names: loadout install --all installs every program this
machine maps (no scripts โ that's setup-new-machine's job), and
loadout run --all runs every script it opted into, still check-gated.
Neither upgrades anything: converging installs what's missing, and moving
an installed program to a newer version is a different verb โ upgrade,
in the weekly section below.
Adding gear to the loadout
A new tool crosses your feed. The loop, start to finish:
-
Declare it โ one fragment, usually a few lines (workmux's is a handful of lines: a description, a
depends-on, and a tap-qualified brew command; check and probe derive from the installer). -
Equip the fleet โ list it under
brewin theinstall_withof the profile(s) it belongs to:profiles/omarchy.yamlandprofiles/macos.yaml. Every current and future machine extending them now carries it. -
Trust nothing โ
loadout explain workmuxshows exactly what the engine resolved (including whether its installer is one of loadout's built-ins or your repo's own โloadout installers brewprints the mechanics either way);loadout install workmux --dry-runshows exactly what would run.$ loadout explain workmux program workmux โ git worktrees + tmux windows for parallel agent sessions depends-on tmux, brew-taps install.brew sh "$LOADOUT_REPO/programs/brew/brew.sh" install raine/workmux/workmux [installer: brew (repo)] <- t2-macbook-omarchy observed missing (state/t2-macbook-omarchy.json) -
loadout install workmux, then commit the manifest change. Other machines pick it up at their nextsyncand show itmissinguntil they converge โ visible drift, never silent divergence.
Weekly-ish: what do the remotes offer?
$ loadout outdated
โ omarchy 168 updates ยท 2 in your loadout loadout upgrade omarchy โ omarchy update -y
โ brew 6 updates ยท 4 in your loadout loadout upgrade brew โ brew upgrade
PROGRAM CURRENT CANDIDATE SOURCE
โ brave 1.82.1 -> 1.82.3 [omarchy-aur]
โ kitty 0.48.2 -> 0.49.0 [omarchy]
โ loadout 2.0.0 -> 2.0.1 [release]
outdated is the one command whose job is asking the outside world โ each
installer's batch oracle in a single call, your repo's own
custom outdated sources,
plus the tool itself as a row (loadout is part of the loadout). The top
lines count each package manager's whole answer, and the table lists only
what your loadout declares. Converging never moves these: a newer version of
something already installed is not setup-new-machine's or install --all's
job, since both only install what's missing. Status also nags in its footer
when the binary falls behind, so even this is hard to miss.
Acting on it
Programs move a whole mechanism at a time:
$ loadout upgrade omarchy omarchy-aur brew
Upgrade on t2-macbook-omarchy:
โ [omarchy] omarchy update -y
covers 45 mapped program(s): 1password, anydesk, atuin, bat, โฆ
โ [brew] brew upgrade
covers 6 mapped program(s): awscli, just, scrcpy, โฆ
This upgrades everything each mechanism manages โ including packages loadout doesn't declare.
Proceed? [y/N]
There is deliberately no loadout upgrade kitty: partial upgrades are
unsupported on Arch, discouraged on Fedora and pointless elsewhere, since the
package manager resolves its own transaction anyway โ naming a program is an
error pointing at its mechanism. Mechanisms that share a command
(omarchy and omarchy-aur both run omarchy update -y) are one
step, named by the tool you know โ [omarchy]; --all takes every
mechanism this machine maps, and only those โ a machine whose loadout
never touches brew can't run brew upgrade by accident. Afterwards every
mapped program is re-checked and the state file rewritten, because the
transaction moved what it moved.
Custom sources go the other way. A pin in mise's config or a plugin clone
shares nothing with its neighbours, so a source that declares
upgrade = "... {item}" updates one row at a time โ the
mise-tools oracle
moves a single pin to the newest release within its major (ruby 3.4.8 โ
3.4.10, never 4.x โ crossing a major is a decision, not an update). When
another tool owns the pins, the row hands the update back to it: the live
repo's
nvim-plugins
asks lazy.nvim to move one plugin, lockfile and all. A source with no
upgrade at all keeps read-only rows โ it reports, and the fix is yours.
The home screen puts both under one table, grouped by what will act.
Open remote with l: each package manager gets a line with everything
it reported โ not only your programs โ and the exact sweep it would run;
your declared programs sit under it, and an amber line names what else the
sweep would touch โ enter on it opens the whole list in a scrollable pane.
That line is the point: ticking omarchy means those 168, and the screen
says so before you press enter. Custom sources follow, one tick per item
or the heading's tick for all of them at once; rows that nothing can move
show [โ]. space ticks, a ticks everything, u clears:
! remote omarchy 168 ยท brew 6 ยท 3 more ยท mise-tools 2 โพ review them
โฏ [x] omarchy 168 updates ยท 2 in your loadout omarchy update -y
โ brave 1.82.1 -> 1.82.3
โ kitty 0.48.2 -> 0.49.0
+ 166 more not in your loadout ยท enter lists them
[ ] brew 6 updates ยท 4 in your loadout brew upgrade
โ just 1.40.0 -> 1.41.0
โ yq 4.47.1 -> 4.47.2
+ 2 more not in your loadout ยท enter lists them
[ ] tmux-plugins
[ ] โ tpack 27172d5 -> 1f8b694 2 commit(s) behind โ
[ ] mise-tools
[x] โ ruby 3.4.8 -> 3.4.10
[ ] โ node 22.11.0 -> 22.12.0
[ ] nvim-plugins
[ ] โ gitsigns.nvim 5eb287f -> 8d79f24 2 commit(s) โ
The remote line leads with the managers that have work, because each of
those is one command; everything that takes an update of its own is
counted after it.
A row marked โ has a page about the change โ for a git plugin, the
GitHub compare of the two commits โ and K opens it in your browser
before you decide, the way K does in lazy.nvim. A long table folds:
h on any row of a group tucks it behind its heading (omarchy โธ), l on
the heading opens it again, and h again on a folded heading closes the
whole remote row โ as does esc from anywhere in the table.
loadout outdated prints the same per-tool lines first, so the shell
answer to "how far behind is this machine, all of it?" is one command.
Enter asks first, one line per step:
โญโ run this? โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ These will upgrade, in order: โ
โ โ
โ omarchy all 168 packages ยท 2 in your loadout โ
โ mise-tools ruby 3.4.8 โ 3.4.10 โ
โ โ
โ Run Cancel โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Run hands your terminal to loadout upgrade (a custom source's rows go
as --item mise-tools/ruby), which prints the exact commands first:
โโ loadout upgrade omarchy-aur omarchy --item mise-tools/ruby --yes โโโโโโโโ
Upgrade on t2-macbook-omarchy:
โ [omarchy] omarchy update -y
covers 45 mapped program(s): 1password, anydesk, atuin, bat, โฆ
โ [mise-tools: ruby] sh '.loadout/outdated-tools.sh' update ruby
A step needs your sudo password.
[sudo] password for jose:
==> upgrading omarchy
โฆ
==> upgrading mise-tools: ruby
โฆ
โ 2 declared program(s) changed version
brave 1.82.1 -> 1.82.3
kitty 0.48.2 -> 0.49.0
โโ โ done โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
enter returns to loadout ยท v views the full output ยท q quits
It's a real terminal: sudo asks for its password, once, before the first
step that needs it, and a package manager's
own questions (a reboot prompt, a conflict) wait for your answer.
Ctrl-C stops the command, not loadout. v pages through the whole
output; Enter returns to the screen as you left it, with the upgraded
ticks cleared and the remote row asking again.
The tool itself is loadout self-upgrade (or U on the home screen): it
needs no repo, so it works even when a version floor refuses everything
else.
New machine day
The payoff for all the declaring. On a fresh Mac:
$ curl -fsSL https://raw.githubusercontent.com/josemiguelo/loadout/master/install.sh | sh
$ git clone [email protected]:you/dotfiles.git ~/.local/share/chezmoi
$ export LOADOUT_REPO=$HOME/.local/share/chezmoi # into your shell profile
$ echo 'extends: [macos]' > $LOADOUT_REPO/machines/$(hostname).yaml
$ loadout setup-new-machine
$ loadout sync
One line of config, because the machine is standard issue: its whole loadout
comes from the
macos profile.
setup-new-machine refuses to start if anything is unmapped or a required
package manager is missing โ errors before actions, with the fix in the
message. Setup-only bootstraps (like the first dotfiles clone) run here and
then never bother you again.
When the fleet needs a newer tool
Raising min-tool-version in the repo is how you roll the fleet forward
deliberately โ a repo that leans on a newer binary's built-in installers, or
on any newer manifest feature, declares the floor in the same commit. A
machine that pulls the new floor with an old binary refuses
everything with one instruction โ run: loadout self-upgrade โ and that
command needs no repo, so it works even mid-refusal. Machines that keep up
never see the refusal at all: the status footer and outdated had already
nagged them past it.
The loop, compressed
declare (edit repo, commit) โ converge (setup-new-machine / install / run)
โ โ
diff (fleet view) โ publish (sync) โ observe (status) โ upgrade (outdated โ upgrade)
Bare loadout is that loop as a screen: observe on open, converge or
upgrade on enter, publish on S.
Every arrow is explicit, every state is inspectable, and every "is it really?" has a check answering it. That's the whole idea: a machine is never mysterious โ it's just carrying its loadout, or visibly not.