Add 'bevel migrate' so bevel works on a fresh machine
## The bug
On a machine that has never had a `bevel` subcommand run on it, bevel does not
work. `bevel-gather` fails with `Failed to insert command: unable to open
database file` (or `no such table: command` if the directory happens to exist),
so nothing is ever recorded and there is nothing for `bevel sync` to sync. The
workaround was to run `bevel sync` once, let it fail, and rely on the
migrations it applied on the way in.
## Root cause
`bevel-gather/bevel-gather.c` only ever `INSERT`s and `UPDATE`s rows in the
`command` table. It never creates the schema, and it does not create the data
directory either.
The only place the schema was ever created was `bevelCLI`'s `runC` in
`bevel-cli/src/Bevel/CLI.hs`, which every subcommand goes through. So the
schema appeared as a side effect of running any `bevel` subcommand, and never
otherwise. The home-manager module installed the harness and, when
`sync.enable` was set, a `bevel-sync` service and daily timer, but nothing ran
the migrations at activation time.
## The test
`nix/nixos-module-test.nix` now records a command with `bevel-gather` right
after home-manager activation and asserts the row lands in the database. It is
placed before the existing `bevel register` / `login` / `sync` steps, because
those are exactly what used to hide the bug.
Committed separately in 0402393, where it fails with the error above.
There is also a cheap Haskell-level test in `bevel-cli/test/Bevel/CLISpec.hs`
covering the new subcommand on its own: `bevel migrate --database <nested path>`
followed by a `bevel-gather` run against that database.
## The fix
- New `bevel migrate` subcommand. `bevelCLI` is restructured so that taking the
database lock and opening the pool (`withPool`) is separate from applying the
migrations (`migrateDb`); `migrate` takes the exclusive lock and does only
the latter. Every other subcommand still migrates first, as before.
- `nix/home-manager-module.nix` runs it in two places:
- a `home.activation.bevelMigrate` script, and
- a `bevel-migrate` user service wanted by `default.target`, which
`bevel-sync` now `Wants` and is ordered `After`.
The activation script is the part that actually closes the hole, and this is a
deviation from the systemd-service-only approach we discussed. The user's
systemd instance is not necessarily running while home-manager activates: on
the fresh VM in the module test, activation logs `User systemd daemon not
running. Skipping reload.` and no user unit runs at all. Even with a live user
manager, `default.target` is reached concurrently with the first login shell, so
the service alone leaves a race for the first few commands after boot. The
service is still worth keeping for the reboot case and for the ordering
guarantee ahead of `bevel-sync`.
Note that `bevel-migrate` is now created whenever `programs.bevel.enable` is
set, not only when `sync.enable` is, since gathering needs the schema
regardless of whether syncing is on.
## Things to check manually
- Deploy to `tv` and confirm gathering works without ever running `bevel`
by hand.
- `bevel migrate` failing now fails `home-manager switch`. I think a loud
failure beats the silent one we had, but it is a behaviour change worth a
second opinion: a corrupt history database would block switching the whole
home configuration.
- `programs.bevel.config.database`, if set, is honoured by `bevel migrate`
(via `BEVEL_CONFIG_FILE`) but not by `bevel-gather`, which only looks at
`BEVEL_DATABASE` and `XDG_DATA_HOME`. That divergence predates this change,
but it now means the migrated database and the gathered-into database can be
different files. Same for a non-default `xdg.dataHome`.
- Whether `bevel-gather` should create the schema itself, so nothing outside it
has to be arranged correctly first. Out of scope here.
## Base branch
Opened against `master`: `origin/development` is 8 commits behind `master` and
`origin/HEAD` points at `master`, so a PR onto `development` would have shown
those 8 commits as part of the diff.