How it works
Updating
devmachine update does five things in a row. Most of what it does is plain;
these are the parts that are not.
It stops before your machines
Everything before the last step changes only your computer: the CLI, one line
in config.yml, the skills in your home folder. The last step changes
servers, and servers run things people are using. So update runs sync --check first, shows what would change, and asks one question.
No is the default. An empty answer, closed input, or no terminal at all
(a cron job, a pipe) is also no, and update prints the exact command to
run when you are ready. A machine never changes on an answer nobody gave.
--yes exists for automation you already trust to change servers.
The new CLI runs the rest
When the CLI itself is out of date, update replaces it and then starts the
new binary to finish the job. The old process is still running the old code
in memory; if it went on, steps 2 to 5 would run with whatever bugs the new
release fixed. The new binary gets the same command line plus one hidden flag
naming the version it replaced, so it skips step 1 and reports 0.7.17 → 0.7.18. If the “new” binary turns out to be the same version, that is
reported as a failure, not as success.
Only the CLI, when that is all you asked for
update --cli-only is step 1 and nothing more. It is what the Devmachine
app runs behind its “Update CLI” button: the app replaces a program on your
computer for you, but moving the packages pin or syncing a server stays
something you start yourself. So --cli-only reads no configuration and
refuses --yes, --skip-… and --machine instead of ignoring them. The
new binary is still started after the swap, with --cli-only, only to
confirm the version changed.
Your computer only, for the app
update --no-machines is steps 1 to 3: the CLI, the packages pin and the
skills. It is what the Devmachine app runs to bring your computer up to
date. It never runs doctor or sync --check, and never asks anything.
The line it draws is the same one as above: before step 4, only your
computer changes. A sync changes servers that people are using, and the app
starts one only when you ask, on the machine you choose. So the app’s
update stops at the line, and refuses --yes and --machine, which are
about servers. With --format json it prints one object with the before
and after of each step, so the app can show what changed without reading
the log.
Why the CLI tells you a newer packages release is out
devmachine update moves the packages pin. But many people never run it:
they upgrade the CLI with brew upgrade, or with the app’s “Update CLI”
button, which runs update --cli-only. Both replace the binary and nothing
else, so the pin and the skills stay where they were, and nothing said a
newer packages release was out. doctor says it, but only to those who run
it.
So after a few everyday commands (sync, doctor, machines list,
workspaces list) the CLI prints one line on stderr:
packages v33 is out (you pin v32): run devmachine update
It is a hint, not a warning, so it stays out of the way:
- At most once a day. The same line on every command is noise people learn to skip.
- Again on the first run of a new CLI version, whatever the daily limit.
That is the moment right after
brew upgrade, when the pin is most likely behind. - Never with
--format jsonand never when stdout or stderr is not a terminal, so a script or the app never reads it as output. DEVMACHINE_NO_UPDATE_HINT=1turns it off.
GitHub is asked at most once a day for the hint, and for no more than three
seconds. The answer is kept in your cache folder next to the one doctor
keeps, and packages outdated reads the same answer. When GitHub cannot
be asked, the hint says nothing: offline is not news. The time it asked is
written before it asks, so a cache folder that cannot be written means the
hint never asks, not that it asks on every command.
Homebrew’s binary goes through Homebrew
A binary Homebrew installed (the real file lives in Homebrew’s Cellar,
under /opt/homebrew, /usr/local or whatever brew --prefix says) is
upgraded with brew upgrade. Writing a new file over it by hand would
work once, and then Homebrew would believe in a version that is not there.
brew update runs first so the tap knows about the new release, and
brew trust --formula runs when your Homebrew has it — newer Homebrew
refuses a third-party tap without it. The same steps install.sh takes.
Any other binary is replaced from the release archive. The archive is checked
against the release’s checksums.txt before anything is written; a mismatch
stops the update and leaves the old binary alone. The new file is written
next to the old one and renamed over it, so there is never a moment with a
half-written devmachine on your PATH.
Packages before skills
The skills your coding agent reads come from the packages release
config.yml pins. Pinning first, then refreshing the skills, means the skills
match the release your machines are about to get, not the one they are
leaving.
A pin moves only forward
update pins the newest packages release only when yours is older. A pin
that is already the newest, or newer (a release you are testing), stays
where it is. A configuration with no pin stays without one: that is a choice
to use only your own packages, and update does not undo it.
An unreachable machine is not a reason to stop
doctor runs on every machine. Warnings and failed checks are printed and
counted, and the run goes on — one broken credential should not stop the
other machines from being checked. A machine doctor could not reach
(configuration, host key or connection failed) skips its sync check, since
there is nothing to compare against, and the summary lists it as skipped (unreachable).
What doctor finds never changes update’s exit code. update’s job is
its own steps: the CLI, the pin, the skills and the sync. If a missing login
on one machine made update exit non-zero, a script could not tell “the
update failed” from “the update worked and a login is missing”. The summary
line says what doctor found (doctor 2 warning(s), doctor machine far unreachable), and devmachine doctor keeps its own exit code for scripts
that care.
Warn or fail
doctor fails a check only when the machine is unusable: the configuration
is invalid, the host key is unknown or changed, the machine cannot be
reached, or an essential is missing (a supported operating system, Ansible,
and on a self machine a bundle folder sync can write). Each of those stops
sync itself.
A missing login or secret, and a DNS provider whose token stopped working, is
a warn. The machine still works; only the tool that needs the login does
not, and the detail says the command that fixes it.
Old is a warning, and offline is a skip
doctor also says when the CLI or the packages pin is behind. That is a
warn, never a fail: an old CLI still works, and a script that runs
doctor should not start failing the day a release comes out. When GitHub
cannot be asked, both checks skip — being offline says nothing about your
versions.
GitHub allows 60 unauthenticated API requests an hour, and doctor runs
often, so its answers are kept for 6 hours in your cache folder. update
always asks GitHub again, and stores what it hears, so doctor is current
right after an update.