Compare commits

..

2 Commits

Author SHA1 Message Date
Julien Calixte
13d49930cc docs: document the SD card provisioning recipes
Record init/load/provision in the roadmap (the config-on-SD migration
and the firmware env!()->file read TODO) and in the git-sync note as the
implementation of the pre-seed-from-a-computer decision.
2026-07-11 01:44:44 +02:00
Julien Calixte
78958d9de6 feat(justfile): split SD provisioning into init/load/provision
Replace the single seed-sd recipe with three entry points sharing
private helpers: init (repo + config), load (repo copy only), provision
(config only). Each excludes gitignored paths via git's own resolution
(so node_modules and firmware/.env never reach the card), writes
/sd/typoena.conf from firmware/.env (Wi-Fi + PAT + git identity, PAT
never printed), and ejects the card so it goes straight into Typoena.
2026-07-11 01:44:38 +02:00
3 changed files with 145 additions and 56 deletions

View File

@@ -80,6 +80,23 @@ pre-seed (not depth-1) also sidesteps the shallow-push sharp edge. remanso
keeps working, the device gets everything, and repo size stops being anyone's
problem.
Implemented in firmware/justfile as three recipes, each ejecting the card when
done so it goes straight into Typoena:
- `just init <repo-path>` — full prep of a fresh card: copies a full clone to
the card's `repo/`, excluding everything the repo's `.gitignore` ignores (so
`node_modules` and local secrets like `firmware/.env` never land on the card),
then writes `/sd/typoena.conf` — Wi-Fi creds + PAT + git identity — from the
TW_* vars already in `firmware/.env` (no re-typing, no prompts).
- `just load <repo-path>` — the repo copy on its own (refresh after big upstream
changes).
- `just provision` — the config on its own (rotate the PAT / switch networks
without re-copying the repo).
The firmware still reads those values via `env!()` today; reading
`/sd/typoena.conf` at boot is a TODO that rides with the SD wiring into
`main.rs`.
## What happens on an ongoing pull
In the single-writer model the device usually doesn't pull at all:

View File

@@ -115,12 +115,20 @@ lives in the `git_sync` / `sd_fat` spike bins, not `main.rs`.
- [~] ESP32-S3 boots (✓); e-ink shows Typoena splash + boot log — splash pending Spike 9
- [x] USB host enumerates the Nuphy, key events reach the editor (Spike 4)
- [ ] One hard-coded file (`/sd/repo/notes.md`) opens on boot — SD spike-only, not in `main.rs`
- [ ] One hard-coded file (`/sd/repo/notes.md`) opens on boot — SD spike-only,
not in `main.rs`. The card is pre-seeded from a computer (`just init`
copies a full clone to `/sd/repo` + writes config), never cold-cloned on
device — see [note](notes/git-sync-images-and-repo-size.md).
- [x] Insert-only editing, backspace, enter, arrow keys — modal editor overshot this early (see v0.2)
- [x] Line wrap, no line numbers yet — soft-wrap done early (see v0.6)
- [ ] Save on `Ctrl-S` → SD — SD blocked, not wired to `main.rs`
- [x] Wi-Fi credentials + remote URL + PAT + author baked into the binary at
build time via env vars (no NVS, no on-device provisioning UI in v0.1)
- [~] Wi-Fi credentials + remote URL + PAT + author: today baked into the binary
via `env!()` (no NVS, no on-device provisioning UI in v0.1). Migrating to
`/sd/typoena.conf` on the card, provisioned by `just provision` (or
`just init` for a fresh card) from the same `firmware/.env` the build uses
(minimum input — rotate the PAT or switch networks without a reflash, no
card re-copy). Firmware to read it at boot instead of
`env!()` — TODO, rides with the SD wiring into `main.rs`.
- [~] `Ctrl-G` runs: `git add .` → commit with an ISO-8601 timestamp message →
`git push`; on push failure, `git pull --no-edit` then retry the push
(no-op short-circuit when nothing is staged). Proven on device in the

View File

@@ -114,54 +114,61 @@ info:
ports:
@ls /dev/cu.usb* 2>/dev/null || echo "no USB serial device found"
# Pre-seed an SD card with a full clone of the notes repo, copied from a
# computer (the decision in docs/notes/git-sync-images-and-repo-size.md). The
# device never cold-clones 566 MB over Wi-Fi + mbedTLS; a laptop copies the
# clone onto the card via a reader, and the device only ever takes the `open` +
# fast-forward path. Dest on the card is a top-level `repo/`, matching the
# on-device /sd/repo the editor opens (roadmap v0.1).
# ─── SD card provisioning ────────────────────────────────────────────────────
# Prepare an SD card on a computer so it can go straight into Typoena (the
# decision in docs/notes/git-sync-images-and-repo-size.md): the device never
# cold-clones 566 MB over Wi-Fi + mbedTLS; a laptop copies the clone onto the
# card via a reader, and the device only ever takes the `open` + fast-forward
# path. Three entry points, each ejecting the card when done:
#
# just seed-sd ~/code/notes # auto-detect the one removable card
# just seed-sd ~/code/notes MYSD # or name /Volumes/<name> if >1 card
# just init ~/code/notes # full prep of a fresh card: repo + config
# just load ~/code/notes # (re)copy just the notes repo → /sd/repo
# just provision # (re)write just the config from firmware/.env
#
# Everything the repo's .gitignore ignores is excluded (node_modules is 3.9 GB,
# gitignored, and never in .git) — the device needs .git + the checkout
# (~720 MB), not the JS deps or any local secrets like firmware/.env. Copying
# (not a fresh `git clone` of the local path) preserves origin → GitHub so
# on-device fetch/push still work; the recipe warns if origin isn't a remote.
# Add a /Volumes/<name> as the last arg if more than one card is mounted.
# The `_`-prefixed recipes are shared internals (hidden from `just --list`).
sd_repo_dir := "repo"
seed-sd repo_src sd_volume="":
# Full prep of a fresh card: copy the notes repo + write config, then eject.
# Run this once per card. <repo-src> is a clone made on this computer.
init repo_src sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _load-repo "{{repo_src}}" "$vol"
just _write-conf "$vol"
just _eject "$vol"
# Resolve + validate the source clone.
src="{{repo_src}}"
src="${src%/}" # strip trailing slash
[ -d "$src/.git" ] || { echo "error: '$src' is not a git repo (no .git/)"; exit 1; }
# (Re)copy just the notes repo to /sd/repo (full clone, gitignored paths
# excluded), then eject — e.g. to refresh the card after big upstream changes.
load repo_src sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _load-repo "{{repo_src}}" "$vol"
just _eject "$vol"
origin="$(git -C "$src" remote get-url origin 2>/dev/null || true)"
case "$origin" in
https://*|git@*|ssh://*|git://*)
echo "source origin: $origin" ;;
"")
echo "warning: '$src' has no 'origin' remote — the device can't fetch/push after seeding" ;;
*)
echo "warning: origin is '$origin' (looks like a local path, not a remote) —"
echo " the device fetch/push will fail; set origin to the GitHub URL first" ;;
esac
# Opportunistic cross-check against TW_REMOTE_URL (loaded from .env if present).
if [ -n "${TW_REMOTE_URL:-}" ] && [ -n "$origin" ] && [ "$origin" != "${TW_REMOTE_URL}" ]; then
echo "warning: origin ($origin) != TW_REMOTE_URL (${TW_REMOTE_URL})"
fi
# (Re)write just the config (Wi-Fi + PAT + git identity) from firmware/.env,
# then eject — e.g. to rotate the PAT or switch networks without touching repo/.
provision sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _write-conf "$vol"
just _eject "$vol"
# Detect the target card. Prefer an explicit /Volumes/<name>; else auto-detect
# exactly one ejectable + external volume (refuse on 0 or >1 — a wrong guess
# here means rsync --delete wipes the wrong disk's repo/).
# Resolve the target card volume. Prints the /Volumes/<name> path as the last
# stdout line (callers capture it); all diagnostics go to stderr. Prefers an
# explicit name; else auto-detects exactly one ejectable + external volume, and
# refuses on 0 or >1 — a wrong guess here means rsync --delete wipes the wrong
# disk's repo/.
_card sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
want="{{sd_volume}}"
if [ -n "$want" ]; then
vol="/Volumes/$want"
[ -d "$vol" ] || { echo "error: '$vol' is not mounted"; exit 1; }
[ -d "$vol" ] || { echo "error: '$vol' is not mounted" >&2; exit 1; }
else
cands=()
for v in /Volumes/*; do
@@ -173,30 +180,51 @@ seed-sd repo_src sd_volume="":
fi
done
case "${#cands[@]}" in
0) echo "error: no removable card detected under /Volumes — insert an SD card (FAT32)"; exit 1 ;;
0) echo "error: no removable card detected under /Volumes — insert an SD card (FAT32)" >&2; exit 1 ;;
1) vol="${cands[0]}" ;;
*) echo "error: multiple removable volumes — name one as the 2nd arg (just seed-sd <src> <name>):"
printf ' %s\n' "${cands[@]#/Volumes/}"; exit 1 ;;
*) echo "error: multiple removable volumes — name one as the volume arg:" >&2
printf ' %s\n' "${cands[@]#/Volumes/}" >&2; exit 1 ;;
esac
fi
# The device won't mount a non-FAT card (esp_vfs_fat, format_if_mount_failed=false).
fs="$(diskutil info "$vol" 2>/dev/null | grep -iE 'File System Personality' | sed 's/.*:[[:space:]]*//' || true)"
case "$fs" in
*FAT32*|*MS-DOS*) : ;;
*) echo "warning: '$vol' is '$fs', not FAT32 — the device may fail to mount it" ;;
*) echo "warning: '$vol' is '$fs', not FAT32 — the device may fail to mount it" >&2 ;;
esac
echo "$vol"
dest="$vol/{{sd_repo_dir}}"
echo "seeding: $src -> $dest"
# Copy a full clone of the notes repo to <vol>/repo. Everything the repo's
# .gitignore ignores is excluded (node_modules is 3.9 GB, gitignored, never in
# .git) — the device needs .git + the checkout (~720 MB), not the JS deps or
# secrets like firmware/.env. Copying (not a fresh `git clone` of the local
# path) preserves origin → GitHub so on-device fetch/push still work.
_load-repo repo_src vol:
#!/usr/bin/env bash
set -euo pipefail
src="{{repo_src}}"; src="${src%/}" # strip trailing slash
[ -d "$src/.git" ] || { echo "error: '$src' is not a git repo (no .git/)"; exit 1; }
origin="$(git -C "$src" remote get-url origin 2>/dev/null || true)"
case "$origin" in
https://*|git@*|ssh://*|git://*) echo "source origin: $origin" ;;
"") echo "warning: '$src' has no 'origin' remote — the device can't fetch/push after loading" ;;
*) echo "warning: origin is '$origin' (looks like a local path, not a remote) —"
echo " the device fetch/push will fail; set origin to the GitHub URL first" ;;
esac
if [ -n "${TW_REMOTE_URL:-}" ] && [ -n "$origin" ] && [ "$origin" != "${TW_REMOTE_URL}" ]; then
echo "warning: origin ($origin) != TW_REMOTE_URL (${TW_REMOTE_URL})"
fi
dest="{{vol}}/{{sd_repo_dir}}"
echo "copying repo: $src -> $dest"
mkdir -p "$dest"
# Build the exclude list from git's own ignore resolution (the repo's
# .gitignore, plus .git/info/exclude and any global excludes). --directory
# collapses a fully-ignored dir (node_modules/, 3.9 GB) to one line, so the
# list stays tiny. Driving off git — not rsync's own dir-merge — means we
# never list a tracked file or anything under .git/, so a .gitignore pattern
# like `logs/` or `*.pack` can't silently corrupt the copied clone.
# Build the exclude list from git's own ignore resolution (.gitignore +
# .git/info/exclude + global). --directory collapses a fully-ignored dir
# (node_modules/, 3.9 GB) to one line. Driving off git — not rsync's own
# dir-merge — means we never list a tracked file or anything under .git/, so
# a .gitignore pattern like `logs/` or `*.pack` can't corrupt the clone.
ignore_list="$(mktemp)"
trap 'rm -f "$ignore_list"' EXIT
git -C "$src" -c core.quotePath=false ls-files \
@@ -209,11 +237,47 @@ seed-sd repo_src sd_volume="":
# -P for progress on the ~700 MB copy. Scoped to repo/, never the card root.
rsync -rtP --delete --modify-window=1 --exclude-from="$ignore_list" "$src/" "$dest/"
# Flush and eject so the card can go straight into Typoena.
# Write <vol>/typoena.conf (Wi-Fi + PAT + git identity) from firmware/.env
# (already loaded via `set dotenv-load`), the same file the build uses — so no
# re-typing creds, no prompts, no network. The PAT is written to the file but
# only ever reported as set/MISSING, never echoed. FAT has no file permissions,
# so physical custody of the card is the control: use a fine-grained PAT
# (contents:write on just the notes repo) so a lost card is a one-token revoke.
_write-conf vol:
#!/usr/bin/env bash
set -euo pipefail
conf="{{vol}}/typoena.conf"
if [ -n "${TW_WIFI_SSID:-}${TW_REMOTE_URL:-}${TW_PAT:-}" ]; then
{
printf '# Typoena runtime config — generated by `just provision` from firmware/.env.\n'
printf '# Plaintext secrets on removable media: keep the card safe; scope TW_PAT\n'
printf '# to contents:write on just the notes repo. `key=value`, `#` = comment.\n'
printf 'TW_WIFI_SSID=%s\n' "${TW_WIFI_SSID:-}"
printf 'TW_WIFI_PASS=%s\n' "${TW_WIFI_PASS:-}"
printf 'TW_REMOTE_URL=%s\n' "${TW_REMOTE_URL:-}"
printf 'TW_GH_USER=%s\n' "${TW_GH_USER:-}"
printf 'TW_PAT=%s\n' "${TW_PAT:-}"
printf 'TW_AUTHOR_NAME=%s\n' "${TW_AUTHOR_NAME:-}"
printf 'TW_AUTHOR_EMAIL=%s\n' "${TW_AUTHOR_EMAIL:-}"
} > "$conf"
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
echo "wrote $conf (values reused from firmware/.env, never printed):"
echo " wifi: ssid=$(mask "${TW_WIFI_SSID:-}") pass=$(mask "${TW_WIFI_PASS:-}")"
echo " git: remote=$(mask "${TW_REMOTE_URL:-}") gh_user=$(mask "${TW_GH_USER:-}") pat=$(mask "${TW_PAT:-}")"
echo " author: name=$(mask "${TW_AUTHOR_NAME:-}") email=$(mask "${TW_AUTHOR_EMAIL:-}")"
else
echo "note: no TW_* values in firmware/.env — leaving any existing $conf untouched"
echo " (copy .env.example → firmware/.env and fill in Wi-Fi + PAT to provision)"
fi
# Flush + eject so the card can go straight into Typoena.
_eject vol:
#!/usr/bin/env bash
set -euo pipefail
sync
echo "seeded ok — ejecting $vol"
if diskutil eject "$vol" >/dev/null 2>&1; then
echo "ejecting {{vol}}"
if diskutil eject "{{vol}}" >/dev/null 2>&1; then
echo "✅ card ejected — remove it and insert into Typoena"
else
echo "⚠️ eject failed (a file may still be open) — eject '$vol' from Finder before removing"
echo "⚠️ eject failed (a file may still be open) — eject '{{vol}}' from Finder before removing"
fi