devmachine

How it works

What the CLI knows about a machine

setup, sync and doctor already connect to a machine. While they are there, they read what it runs — uname, /etc/os-release, sw_vers on a Mac, where ansible-playbook is — in one extra command, and keep the answer in <config>/state/machines/<name>.json. devmachine machines show <name> prints it without connecting, and devmachine --format json machines show <name> gives it to a script or an agent under observed. devmachine machines rm <name> deletes the file with the machine, so a new machine that reuses the name does not inherit what the old one ran.

Why it is not in config.yml

config.yml says what you want. This file says what a command saw, and the next sync reads the machine again. Mixing the two would make a reinstalled server look like a change to your configuration, and an autocommit would record it as one. So the file lives under state/, which devmachine setup git keeps out of the repository. A repository made before this release gets the .gitignore line the next time you run devmachine setup git.

It lives in the configuration directory, like known_hosts, and not in ~/.local/state, because a machine name is unique only inside one configuration: two configurations can each have a machine called vps.

Why it is written only when it changes

The macOS app watches the configuration directory and reloads on every change. A doctor that rewrote the file on each run would make the app reload for nothing. So the file is written only when something other than the time differs, and observed_at is when the machine was first seen as the file describes it — not the last time somebody looked. The file is written to a temporary name in the same folder and then renamed, so a reader never sees half of it.

What reads it

  • run puts path_prefix in front of PATH. A plain SSH command on a Mac gets /usr/bin:/bin:/usr/sbin:/sbin and nothing else, so port, brew and the Python they installed are not found without it. On Linux the list is empty and run sends the command as it is.
  • Every other command the CLI runs on a Mac by name gets the same prefix, plus /Applications/Tailscale.app/Contents/MacOS last, where Tailscale’s app keeps its CLI: login’s command (gh auth login, tailscale up), a network package’s join and self_name, and expose’s caddy validate and caddy reload. expose writes the prefix inside the script it pipes to sudo, because sudo replaces the PATH it was started with.
  • sync refuses, before it changes anything, a package whose platforms leave out the machine’s system — see troubleshooting. A machine nobody has read yet is not refused: its first sync reads it.
  • On a Mac reached over SSH, sync calls Ansible by ansible_playbook, the absolute path the package manager package’s bootstrap reported during setup. A MacPorts install can name it ansible-playbook-3.14, which no PATH would find. On Linux and on your own computer it is ansible-playbook from PATH, as before.
  • An agent reads observed before it writes a run command, so it uses pacman on Arch, apt on Debian and Ubuntu, and brew or port on a Mac. A distribution based on one of them, such as Manjaro, gets its base’s os_family and pkg_mgr and keeps its own name in distribution.

A command never fails because this file could not be written: it is a by-product of a read that already worked.