himalaya/cairn/log/2026-08-14-first-run-wizard-frame.md
2026-08-15 19:50:39 +02:00

31 lines
4.2 KiB
Markdown

---
cairn: log
change: first-run-wizard-frame
landed: 2026-08-14
---
# Met the newcomer the way Comodoro does
Himalaya's wizard took Comodoro's frame. Discovery is untouched, so what an account ends up being is decided exactly as before; what changed is everything around that: how the wizard is reached, what it says on the way in, where it writes, and what happens when it is declined.
`himalaya configure` (alias `wizard`) now runs the wizard by name, so a second account no longer means a bare invocation, and declining the first-run offer is recoverable. The 1.x promise of a configure command is kept.
The offer became a hook. `resolve_account` used to prompt, run the wizard and call `exit(0)`, so the command someone typed never ran, whatever they answered: configuring successfully and then getting nothing is the same outcome as declining. It now offers, re-reads the configuration and carries on, so accepting gives the command a chance to work and declining leaves it to fail the ordinary way. The re-read matters because the wizard can also print the account instead of writing it, so having run it proves nothing landed.
Both entry points check `stdin().is_terminal()` and `printer.is_json()` first. A cron job or a `--json` caller hitting a missing configuration now gets an error it can read rather than a prompt nobody can answer, and `configure` itself refuses outright, naming the sample to write by hand.
The welcome moved onto the offer and names the path that was actually looked at, which is where `-c` or `HIMALAYA_CONFIG` pointed or the default location. Someone who mistyped a path sees the typo rather than a generic first-run banner. The command asked for by name skips the welcome.
Writing stopped prompting for a path and stopped being able to overwrite. The target comes from `Config::target_path`, a file that is not there is written whole, and one that is gets a plain text append, which is the only write that provably leaves a hand-written document alone. Two invariants guard it, both properties of the shared accounts table rather than of Himalaya: a duplicate `[accounts.<name>]` makes the whole file unparseable, and a second `default = true` makes the account every command picks depend on map ordering. The derived name is suffixed until free, which it has to be the moment appending exists.
The three resolution failures each name what is missing and what to do: the path that was read, the accounts the configuration does hold, and the two ways to pick a default.
Rendering gained an ordering pass. The serializer still decides what is written, so a defaulted field is omitted and nothing is enumerated twice, but the flattened dotted keys came out alphabetically: `imap.sasl.plain.authcid` sat above `imap.server`, burying the endpoint under the credentials that authenticate against it, and every group ran together. Groups now run most-defining first, `server` is lifted to the top of its own, and a blank line separates them. A group nobody listed renders after the known ones rather than being dropped, so a field added to `AccountConfig` can never go missing from a generated document because this table went stale.
`HIMALAYA_CONFIG` works again, through a `ConfigPathsArg` declared here rather than pimalaya-cli's shared `ConfigFlags`, since the variable has to carry the product name. Rewriting one arg per repository is cheaper than parameterising the shared one, and it is what Comodoro does. This needed clap's `env` feature, which Himalaya had not enabled.
Deliberately not done: discovery itself, which is specific to each product and is the half this change does not touch.
Verified: build, fmt and clippy clean; 93 tests pass, five of them new. The non-interactive paths were exercised end to end: a bare invocation with no configuration prints the help without prompting, `configure` refuses and names the sample, and the three resolution failures each print what they promise, including through `HIMALAYA_CONFIG`.
Spec updated: wizard (ADDED: A named command runs the wizard, The offer is a hook not a gate, The welcome names the missing path, Generating never rewrites what a human wrote, A generated account reads in a deliberate order; MODIFIED: Account resolution failures name what is missing; REMOVED: The wizard prompts for its target path).