Files
typewriter/firmware/justfile
Julien Calixte 02b7ed88b6 fix(provisioning): rewrite the card origin to HTTPS at load
The device's libgit2 has no SSH transport (HTTPS+PAT over mbedTLS
only), but _load-repo copied the desktop clone's origin verbatim — an
SSH-shaped URL fails every on-device push/fetch with "unsupported URL
protocol" (first real-repo :sync, 2026-07-13). Derive the HTTPS
equivalent for the card copy (git@host:path and ssh://git@host/path);
the source clone keeps its own URL. Warns on non-github hosts, since
the embedded trust store carries GitHub roots only.
2026-07-13 00:53:50 +02:00

545 lines
26 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Typoena firmware — common commands.
# Recipes source ~/export-esp.sh themselves (LIBCLANG_PATH + Xtensa GCC),
# so no per-shell setup is needed before calling just.
# Load firmware/.env (Wi-Fi creds for the network spikes) into recipe env.
# Absent .env is fine — the editor build ignores TW_* entirely.
set dotenv-load := true
esp_env := ". ~/export-esp.sh &&"
elf := "target/xtensa-esp32s3-espidf/release/firmware"
elf_wifi := "target/xtensa-esp32s3-espidf/release/wifi_tls"
elf_sd := "target/xtensa-esp32s3-espidf/release/sd_fat"
elf_splash := "target/xtensa-esp32s3-espidf/release/splash"
elf_git := "target/xtensa-esp32s3-espidf/release/git_smoke"
elf_git_push := "target/xtensa-esp32s3-espidf/release/git_push"
elf_git_sync := "target/xtensa-esp32s3-espidf/release/git_sync"
# Custom partition table (adds the `storage` FAT partition for the git working
# copy). Only the git-push flash applies it — the editor flash keeps the default
# single-app layout, so this change is scoped to Spike 7's finish.
partition_table := justfile_directory() + "/partitions.csv"
# Spike 7 Path 2 — env for the git2/libgit2 build. LIBGIT2_SRC points at the
# vendored libgit2 submodule (v1.9.4, matches libgit2-sys 0.18.5). pkgconfig/
# holds fake .pc files so libgit2-sys + libz-sys run in system mode and the
# symbols come from the esp-idf libgit2 component. Only the git recipes set this,
# so the editor build never compiles libgit2.
libgit2_src := justfile_directory() + "/components/libgit2/vendor"
git_env := "LIBGIT2_SRC=" + libgit2_src + " LIBGIT2_NO_VENDOR=1 PKG_CONFIG_ALLOW_CROSS=1 PKG_CONFIG_LIBDIR=" + justfile_directory() + "/pkgconfig"
# list recipes
default:
@just --list
# compile (release) — the nominal product build: full firmware WITH git
# publishing (compiles libgit2 + mbedTLS, links git2). For fast iteration on the
# editor / EPD / USB / SD without paying for libgit2, use `build-light`.
build:
{{esp_env}} {{git_env}} cargo build --release --bin firmware --features git
# build + flash + open serial monitor (full firmware, see `build`)
flash:
{{esp_env}} {{git_env}} cargo run --release --bin firmware --features git
# LIGHT editor build — no git: no libgit2 component (LIBGIT2_SRC unset) and no
# git2 crate (`git` feature off), so it builds much faster. `:sync` saves locally
# and skips the push. Use for anything but exercising publish.
build-light:
{{esp_env}} cargo build --release --bin firmware
# light build + flash + open serial monitor (see `build-light`)
flash-light:
{{esp_env}} cargo run --release --bin firmware
# serial monitor only, with decoded backtraces
monitor:
espflash monitor --elf {{elf}}
# Spike 6 — build the Wi-Fi + TLS spike (needs TW_WIFI_SSID / TW_WIFI_PASS in .env)
build-wifi:
{{esp_env}} cargo build --release --bin wifi_tls
# Spike 6 — flash + monitor the Wi-Fi + TLS spike
flash-wifi:
{{esp_env}} cargo run --release --bin wifi_tls
# serial monitor for the Wi-Fi spike, with decoded backtraces
monitor-wifi:
espflash monitor --elf {{elf_wifi}}
# Spike 3 — build the SD/FAT spike (no .env needed)
build-sd:
{{esp_env}} cargo build --release --bin sd_fat
# Spike 3 — flash + monitor the SD/FAT spike
flash-sd:
{{esp_env}} cargo run --release --bin sd_fat
# serial monitor for the SD spike, with decoded backtraces
monitor-sd:
espflash monitor --elf {{elf_sd}}
# SD primitive-op micro-benchmark — attributes the ~700 ms/loose-object write
# floor (docs/tradeoff-curves/sync-commit-staging.md). Writes only to /sd/sdbench.
build-bench:
{{esp_env}} cargo build --release --bin sd_bench
# build + flash + monitor the SD bench
flash-bench:
{{esp_env}} cargo run --release --bin sd_bench
# serial monitor for the SD bench, with decoded backtraces
monitor-bench:
espflash monitor --elf target/xtensa-esp32s3-espidf/release/sd_bench
# git-level micro-benchmark — localizes the ~700 ms/object libgit2 overhead
# (docs/tradeoff-curves/sync-commit-staging.md). Read-mostly on /sd/repo.
build-gitbench:
{{esp_env}} {{git_env}} cargo build --release --bin git_bench --features git
# build + flash + monitor the git-level bench
flash-gitbench:
{{esp_env}} {{git_env}} cargo run --release --bin git_bench --features git
# serial monitor for the git-level bench, with decoded backtraces
monitor-gitbench:
espflash monitor --elf target/xtensa-esp32s3-espidf/release/git_bench
# Spike 9 — build the boot splash spike (no .env needed)
build-splash:
{{esp_env}} cargo build --release --bin splash
# Spike 9 — flash + monitor the boot splash spike
flash-splash:
{{esp_env}} cargo run --release --bin splash
# serial monitor for the splash spike, with decoded backtraces
monitor-splash:
espflash monitor --elf {{elf_splash}}
# Spike 7 Path 2 — build the git2/libgit2 smoke (git2 safe API on device)
build-git:
{{esp_env}} {{git_env}} cargo build --release --bin git_smoke --features git
# Spike 7 Path 2 — flash + monitor the git2/libgit2 smoke
flash-git:
{{esp_env}} {{git_env}} cargo run --release --bin git_smoke --features git
# serial monitor for the git smoke, with decoded backtraces
monitor-git:
espflash monitor --elf {{elf_git}}
# Spike 7 finish — build the on-device git push (Wi-Fi + SNTP + flash-FAT + libgit2)
build-git-push:
{{esp_env}} {{git_env}} cargo build --release --bin git_push --features git
# Spike 7 finish — flash + monitor. espflash applies the custom partition table
# (so the `storage` FAT partition exists) and sets 16 MB flash. Uses espflash
# directly, not `cargo run`, so the table is applied only to this binary.
flash-git-push: build-git-push
espflash flash --monitor --baud 921600 --partition-table {{partition_table}} --flash-size 16mb {{elf_git_push}}
# serial monitor for the git push, with decoded backtraces
monitor-git-push:
espflash monitor --elf {{elf_git_push}}
# Milestone #2A — build the persistent-clone publish cycle (clone + fast-forward push)
build-git-sync:
{{esp_env}} {{git_env}} cargo build --release --bin git_sync --features git
# Milestone #2A — flash + monitor. Applies the same custom partition table as
# git-push (the `storage` FAT partition holds the persistent clone).
flash-git-sync: build-git-sync
espflash flash --monitor --baud 921600 --partition-table {{partition_table}} --flash-size 16mb {{elf_git_sync}}
# serial monitor for the git sync, with decoded backtraces
monitor-git-sync:
espflash monitor --elf {{elf_git_sync}}
# detect board, print chip/MAC/flash size
info:
espflash board-info
# list USB serial devices
ports:
@ls /dev/cu.usb* 2>/dev/null || echo "no USB serial device found"
# ─── 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 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 (derive + prompt)
#
# Config (Wi-Fi + PAT + git identity) needs no firmware/.env: each value is
# resolved from firmware/.env if present, else derived from tools the machine
# already has (git config, gh, the active Wi-Fi network + its Keychain
# password), else asked for at an interactive prompt with the derived value as
# the default. The PAT is always typed by hand — never derived (a broad
# `gh auth token` on a plaintext card would defeat the scoped-token model).
#
# 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"
# Full prep of a fresh card: copy the notes repo, seed the git-tracked config
# files (first-time only), write the runtime 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 _seed-configs "$vol"
just _write-conf "$vol" "{{repo_src}}"
just _eject "$vol"
# (Re)copy just the notes repo to /sd/repo: fast-forward the source clone from
# its remote first, then rsync it across (full clone, gitignored paths excluded)
# and eject — e.g. to refresh the card after the device pushed new notes.
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"
# (Re)write just the config (Wi-Fi + PAT + git identity), then eject — e.g. to
# rotate the PAT or switch networks without touching repo/. Values are derived +
# prompted (see the section header); firmware/.env is an optional override. With
# no repo arg, the git remote is derived from the card's existing clone.
provision sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _write-conf "$vol"
just _eject "$vol"
# 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 removable/SD volume (covers cards
# in a built-in Mac reader, which report an "Internal" bus), 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" >&2; exit 1; }
else
cands=()
for v in /Volumes/*; do
[ -d "$v" ] || continue
info="$(diskutil info "$v" 2>/dev/null || true)"
# Treat a volume as a candidate card if ANY removable/SD signal is
# present. A card in a Mac's *built-in* reader shows Protocol "Secure
# Digital" + Removable Media "Removable", but sits on the "Internal"
# bus with no whole-disk "Ejectable" line — so the old
# "Ejectable: Yes AND external" test missed it entirely. None of these
# match the fixed internal APFS disk; a spurious extra match just
# trips the ">1 — name one" refusal below, which is safe.
if echo "$info" | grep -qiE 'Protocol:[[:space:]]+Secure Digital' \
|| echo "$info" | grep -qiE 'Removable Media:[[:space:]]+(Removable|Yes)' \
|| echo "$info" | grep -qiE 'Ejectable:[[:space:]]+Yes' \
|| echo "$info" | grep -qiE 'Device Location:[[:space:]]+External'; then
cands+=("$v")
fi
done
case "${#cands[@]}" in
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 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" >&2 ;;
esac
echo "$vol"
# 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 — except
# the URL scheme: the card copy's origin is rewritten to HTTPS at the end,
# because the device's libgit2 has no SSH transport (HTTPS+PAT over mbedTLS
# only). The source clone is never touched.
_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
# Refresh the source clone from its remote before mirroring, so the card
# carries the latest — e.g. notes the device pushed since the last load.
# Fast-forward-only and best-effort: a dirty tree, diverged history, detached
# HEAD, or being offline just warns and copies the current on-disk state
# rather than aborting the card prep. Set TW_NO_PULL=1 to skip the pull.
case "$origin" in
https://*|git@*|ssh://*|git://*)
if [ -n "${TW_NO_PULL:-}" ]; then
echo "TW_NO_PULL set — skipping pull, copying current on-disk state"
else
branch="$(git -C "$src" symbolic-ref --quiet --short HEAD || true)"
if [ -z "$branch" ]; then
echo "warning: '$src' is in detached HEAD — skipping pull, copying current state" >&2
elif git -C "$src" pull --ff-only origin "$branch"; then
:
else
echo "warning: 'git pull --ff-only origin $branch' failed (dirty, diverged, or offline) —" >&2
echo " copying the current on-disk state instead" >&2
fi
fi
;;
esac
dest="{{vol}}/{{sd_repo_dir}}"
echo "copying repo: $src -> $dest"
mkdir -p "$dest"
# 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 \
--others --ignored --exclude-standard --directory --no-empty-directory \
> "$ignore_list"
echo "excluding $(wc -l < "$ignore_list" | tr -d ' ') gitignored path(s) (incl. node_modules, .env)"
# -rt (no perms/owner/symlinks — meaningless on FAT), --modify-window=1 for
# FAT's 2 s timestamp granularity, --delete to mirror (re-runs stay clean),
# -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/"
# The device's libgit2 speaks HTTPS+PAT only (mbedTLS — no SSH transport),
# so an SSH origin copied verbatim fails every on-device push/fetch with
# "unsupported URL protocol" (found the hard way, 2026-07-13). Rewrite the
# CARD copy's origin to the HTTPS equivalent; the source clone keeps its
# own URL. Its embedded trust store carries GitHub's roots, hence the
# non-github warning.
case "$origin" in
git@*:*)
host="${origin#git@}"; host="${host%%:*}"
card_origin="https://$host/${origin#git@*:}"
;;
ssh://git@*)
rest="${origin#ssh://git@}"
card_origin="https://${rest%%[:/]*}/${rest#*/}"
;;
https://*) card_origin="$origin" ;;
*) card_origin="" ;;
esac
if [ -n "$card_origin" ]; then
if [ "$card_origin" != "$origin" ]; then
echo "rewriting card origin for the device: $origin -> $card_origin"
fi
git -C "$dest" remote set-url origin "$card_origin"
case "$card_origin" in
https://github.com/*) ;;
*) echo "warning: origin host isn't github.com — the device's embedded trust store" \
"only carries GitHub roots, so on-device TLS will fail without its CA" ;;
esac
elif [ -n "$origin" ]; then
echo "warning: origin '$origin' has no HTTPS equivalent I can derive —"
echo " on-device push/fetch will fail; set it by hand:"
echo " git -C '$dest' remote set-url origin https://github.com/<owner>/<repo>.git"
fi
# First-time setup: seed the two git-tracked config files into the card's repo/
# so they commit + sync on the device's first `:sync`. Writes ONLY a file that is
# absent — a card whose clone already carries `.typoena.toml` /
# `.typoena.snippets.json` keeps its own, so a re-`init` never clobbers a synced
# library. The starter `.typoena.toml` mirrors the editor defaults; the snippet
# library is assembled from the curated catalog (snippets-catalog/, opt-in per
# group) with `jq`. Non-interactive runs take every default (all groups).
_seed-configs vol:
#!/usr/bin/env bash
set -euo pipefail
repo="{{vol}}/{{sd_repo_dir}}"
catalog="{{justfile_directory()}}/snippets-catalog"
interactive=1; [ -t 0 ] || interactive=0
# ask_yn "label" → 0 (yes) / 1 (no); default yes, non-interactive → yes.
ask_yn() {
local ans
if [ "$interactive" = 0 ]; then return 0; fi
read -r -p " $1 [Y/n]: " ans || ans=""
[[ "${ans:-Y}" =~ ^[Yy] ]]
}
# ── starter prefs (.typoena.toml) ────────────────────────────────────────
prefs="$repo/.typoena.toml"
if [ -e "$prefs" ]; then
echo "keeping existing .typoena.toml"
elif ask_yn "seed a starter .typoena.toml (editor defaults)?"; then
# printf per line (matching _write-conf), so no heredoc indentation trap.
{
printf '# Typoena editor preferences — hand-editable, git-tracked.\n'
printf '# Edit here, or change live from the Cmd-P palette (type `>`).\n'
printf 'save_on_idle = true\n'
printf 'format_on_save = true\n'
printf 'line_numbers = true\n'
printf 'theme = "light"\n'
printf 'auto_sync = "10m"\n'
} > "$prefs"
echo "wrote .typoena.toml"
fi
# ── snippet library (.typoena.snippets.json) from the catalog ────────────
snips="$repo/.typoena.snippets.json"
if [ -e "$snips" ]; then
echo "keeping existing .typoena.snippets.json"
elif ! command -v jq >/dev/null 2>&1; then
echo "warning: jq not found — skipping the snippet catalog (install jq, or" >&2
echo " hand-write $snips later)" >&2
else
echo "snippet catalog — choose groups to include:" >&2
picked=()
ask_yn "Symbols (→ ≠ × · ° € œ)" && picked+=("$catalog/symbols.json")
ask_yn "Structure (todo, link, img, table, code)" && picked+=("$catalog/structure.json")
ask_yn "Prose (booknotes, reference, bias, capture, standard)" && picked+=("$catalog/prose.json")
if [ "${#picked[@]}" -gt 0 ]; then
jq -s 'add' "${picked[@]}" > "$snips"
echo "wrote .typoena.snippets.json ($(jq 'length' "$snips") snippets, ${#picked[@]} group(s))"
else
echo "no groups chosen — skipping .typoena.snippets.json"
fi
fi
# Resolve + write <vol>/typoena.conf (Wi-Fi + PAT + git identity). Each value
# runs a ladder: firmware/.env (loaded via `set dotenv-load`) → derived from
# tools already on the machine (git config, gh, the active Wi-Fi network + its
# System-keychain password) → interactive prompt, with the derived value as the
# default. So a newcomer needs no .env: they Enter through the defaults and paste
# a PAT once. The PAT is never derived (a broad `gh auth token` on plaintext
# removable media would defeat the scoped-token model) and 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. <repo_src> (optional) seeds the git remote; without it the
# remote is derived from the card's existing clone.
_write-conf vol repo_src="":
#!/usr/bin/env bash
set -euo pipefail
conf="{{vol}}/typoena.conf"
repo_src="{{repo_src}}"; repo_src="${repo_src%/}"
interactive=1; [ -t 0 ] || interactive=0
# ask "label" "default" → chosen value on stdout; prompt shown on stderr (so
# it survives the $(...) capture). Non-interactive → returns the default.
ask() {
local label="$1" def="${2:-}" ans
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
read -r -p " $label${def:+ [$def]}: " ans || ans=""
printf '%s' "${ans:-$def}"
}
# like ask but hides input (secrets). A present default reads as "keep current".
ask_secret() {
local label="$1" def="${2:-}" ans
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
read -r -s -p " $label${def:+ [keep current]}: " ans || ans=""; printf '\n' >&2
printf '%s' "${ans:-$def}"
}
# ── derive defaults (silent; empty when a tool is missing/unauthed) ──────────
# remote: .env → source repo's origin → the card's existing clone.
remote="${TW_REMOTE_URL:-}"
if [ -z "$remote" ] && [ -n "$repo_src" ] && [ -d "$repo_src/.git" ]; then
remote="$(git -C "$repo_src" remote get-url origin 2>/dev/null || true)"
fi
if [ -z "$remote" ] && [ -d "{{vol}}/{{sd_repo_dir}}/.git" ]; then
remote="$(git -C "{{vol}}/{{sd_repo_dir}}" remote get-url origin 2>/dev/null || true)"
fi
a_name="${TW_AUTHOR_NAME:-$(git config user.name 2>/dev/null || true)}"
a_email="${TW_AUTHOR_EMAIL:-$(git config user.email 2>/dev/null || true)}"
gh_user="${TW_GH_USER:-}"
[ -z "$gh_user" ] && gh_user="$(gh api user --jq .login 2>/dev/null || true)"
# ssid: .env → the Mac's active Wi-Fi network.
wifi_if="$(networksetup -listallhardwareports 2>/dev/null | awk '/Wi-Fi/{getline; print $2; exit}')"
ssid="${TW_WIFI_SSID:-}"
[ -z "$ssid" ] && ssid="$(networksetup -getairportnetwork "${wifi_if:-en0}" 2>/dev/null | sed -n 's/^Current Wi-Fi Network: //p')"
wifi_pass="${TW_WIFI_PASS:-}"
pat="${TW_PAT:-}"
# ── confirm / fill via prompts ──────────────────────────────────────────────
[ "$interactive" = 1 ] && echo "configuring $conf — press Enter to accept each [default]:" >&2
ssid="$(ask "Wi-Fi SSID" "$ssid")"
# Keychain read happens after the SSID is final (the user may have edited it).
# Reading a System-keychain Wi-Fi password can pop a macOS auth dialog — only
# attempt it interactively so scripted runs never trigger a surprise prompt.
if [ -z "$wifi_pass" ] && [ -n "$ssid" ] && [ "$interactive" = 1 ]; then
echo " looking up Wi-Fi password for '$ssid' in Keychain (approve the macOS dialog if it appears)…" >&2
wifi_pass="$(security find-generic-password -wa "$ssid" 2>/dev/null || true)"
fi
wifi_pass="$(ask_secret "Wi-Fi password" "$wifi_pass")"
remote="$(ask "Git remote URL" "$remote")"
gh_user="$(ask "GitHub username" "$gh_user")"
pat="$(ask_secret "GitHub PAT (fine-grained, contents:write)" "$pat")"
a_name="$(ask "Commit author name" "$a_name")"
a_email="$(ask "Commit author email" "$a_email")"
# ── require the essentials (a blank config ships a dead device) ──────────────
missing=""
[ -n "$ssid" ] || missing="$missing Wi-Fi-SSID"
[ -n "$remote" ] || missing="$missing git-remote-URL"
[ -n "$pat" ] || missing="$missing GitHub-PAT"
if [ -n "$missing" ]; then
echo "error: missing required config:$missing" >&2
[ "$interactive" = 0 ] && echo " (no TTY — set these in firmware/.env or run interactively)" >&2
exit 1
fi
# ── write ───────────────────────────────────────────────────────────────────
{
printf '# Typoena runtime config — generated by `just init`/`provision`.\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' "$ssid"
printf 'TW_WIFI_PASS=%s\n' "$wifi_pass"
printf 'TW_REMOTE_URL=%s\n' "$remote"
printf 'TW_GH_USER=%s\n' "$gh_user"
printf 'TW_PAT=%s\n' "$pat"
printf 'TW_AUTHOR_NAME=%s\n' "$a_name"
printf 'TW_AUTHOR_EMAIL=%s\n' "$a_email"
} > "$conf"
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
echo "wrote $conf (secrets never printed):"
echo " wifi: ssid=$(mask "$ssid") pass=$(mask "$wifi_pass")"
echo " git: remote=$(mask "$remote") gh_user=$(mask "$gh_user") pat=$(mask "$pat")"
echo " author: name=$(mask "$a_name") email=$(mask "$a_email")"
# Flush + eject so the card can go straight into Typoena.
_eject vol:
#!/usr/bin/env bash
set -euo pipefail
sync
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"
fi