feat(installer): self-contained macOS SD-card setup CLI

A ratatui TUI that provisions the SD card for a pre-flashed Typoena:
Preflight → Configure → SD card → Done. Clones the notes repo onto the
card with system git (PAT in http.extraHeader, never in origin), seeds
.typoena.toml, writes typoena.conf, strips AppleDouble, ejects. An
already-provisioned card is handled by a y-confirmed wipe-and-reclone
that only ever removes repo/ + the dirty journal.

This is the target of typoena.dev's `curl … | sh`. Also records firmware
auto-update options in docs/macroplan.md (v1.x).
This commit is contained in:
Julien Calixte
2026-07-14 15:56:34 +02:00
parent 99574ef00c
commit 691319cbd1
11 changed files with 3173 additions and 1 deletions

95
installer/DESIGN.md Normal file
View File

@@ -0,0 +1,95 @@
# Typoena installer — design
A self-contained macOS CLI (ratatui TUI) that prepares an SD card so a
**pre-flashed** Typoena is ready to use the moment the card goes in. The public
entry point is the one-liner on typoena.dev:
curl -fsSL https://typoena.dev/install.sh | sh
`install.sh` downloads this prebuilt binary; the binary does the rest. The user
needs **no repo checkout and no Rust toolchain** — just the card.
## Decisions (2026-07-14)
- **Self-contained end-user tool.** No `just`, no typewriter checkout. The
binary bundles what it needs (config templates, snippet catalog). The proven
`just` bash (`firmware/justfile`) is the *reference spec* for the safety
behaviours, ported to Rust — not shelled out to.
- **The installer never flashes.** Devices ship **pre-flashed from
manufacturing**; setup is SD-card-only. Firmware field updates
(auto-update) are a **device/roadmap** concern, not the installer's — see
[`docs/macroplan.md`](../docs/macroplan.md) (v1.x note).
- **The card's repo is a fresh `git clone` from the remote** (HTTPS + PAT),
written straight onto the card. There is no local source clone to mirror, so
none of the rsync machinery applies: **no `--ff-only` refresh, no `.gitignore`
excludes, no repack** — a fresh clone already contains only tracked files and
is a single pack, and its origin is already the HTTPS URL we cloned from.
- **Lives in `typewriter/installer/`**, tracked in the firmware repo so it
versions in lockstep with the config templates + snippet catalog it ships.
## Phase pipeline (the wizard)
1. **Preflight** — a mounted card + git present. Advisory; warnings don't block.
2. **Configure** — collect Wi-Fi SSID/pass, git remote, GitHub user, PAT, and
commit identity. Pre-fill via the derive ladder (below).
3. **SD card** — pick the card (refuse on ambiguity), `git clone` the remote
onto `/repo`, seed `.typoena.toml` + snippets if absent, write
`typoena.conf`, strip `._*`, eject.
4. **Done** — "put the card in your Typoena and power on."
## Safety behaviours to keep (from `firmware/justfile` — do NOT regress)
- **Card-ambiguity refusal** — never guess when >1 removable volume; a wrong
guess lets a write hit the wrong disk. Refuse and ask.
- **`.typoena-dirty` guard** — refuse to overwrite a card that carries
unpublished device edits; offer backup-and-discard.
- **AppleDouble `._*`** — `dot_clean` before eject; `._pack-*.idx` corrupts the
pack scan (Mac git *and* device libgit2).
- **PAT never derived** — always typed; fine-grained, `contents:write` on the
one repo; plaintext on FAT means physical custody is the control. The clone
uses the PAT so private notes repos work.
_Dropped vs. the old `just load` (now moot with clone-from-remote): rsync
mirror, `--ff-only` source refresh, `.gitignore` exclude list, `git repack -ad`._
## Config derive ladder (Configure step)
Each value: explicit input → derived from this Mac → prompt.
`author``git config user.{name,email}` · `gh_user``gh api user` ·
`ssid` ← active Wi-Fi (`networksetup`) · `wifi_pass` ← Keychain (on ^K, may
prompt macOS) · `remote`, `pat` ← typed (PAT never derived).
## Architecture / crates
- `ratatui` + its crossterm backend — TUI.
- `git2` — clone the remote onto the card, confirm origin. _[SD slice]_
- Config templates + snippet catalog embedded via `include_str!`
(self-contained).
## Open items (not blocking the current slices)
- **Hosting** — where the CLI binary lives for the `curl | sh` download (Gitea
release vs typoena.dev static asset). Dev runs via `cargo run` meanwhile.
- **Non-macOS** — Linux/Windows later; slice work is macOS-first.
- **Clone target** — cloning ~hundreds of MB directly onto FAT via a reader;
measure, and fall back to clone-to-temp-then-copy if it's too slow.
- **Re-provision** — DONE for the destructive case: an existing card is handled
by an explicit **wipe-and-reclone** (`y`-confirmed screen showing origin +
HEAD + unpublished-edit count; removes only `repo/` + the dirty journal, then
clones fresh). Follow-ups: a config-only rewrite that rotates the PAT /
switches Wi-Fi *without* recloning (like `just provision`), and backing up
`.typoena-dirty` edits before wiping instead of only warning.
## Slice plan
1. **App shell + Preflight** — DONE 2026-07-14. Branded wizard; card + git
detection; `--check` headless mode.
2. **Configure** — DONE 2026-07-14. Form + derive ladder, masked secrets,
Keychain fill, required-field validation.
3. **SD card** — DONE 2026-07-14 (fresh-card path). Pick card (boot disk
excluded) → `git clone` onto it (single pack, clean HTTPS origin) → seed
`.typoena.toml` → write `typoena.conf` → strip `._*` → eject; the long clone
runs on a worker thread streaming progress. Verified: card detection on real
hardware and clone + seed + conf via `--list-cards` / `--dry-run-sd`. Full
interactive run + real write/eject await a blank card + a TTY.
4. **install.sh + release/hosting** — checksums, polish.