Shell 785 lines
# FlashOS shell helpers. Source from ~/.zshrc:
# [[ -f /path/to/FlashOS/flashos.env.zsh ]] && source /path/to/FlashOS/flashos.env.zsh
#
# Public interface — two verb dispatchers plus a build wrapper:
# pi connect|capture|list|quit|log|tail serial-console helpers (Raspberry Pi)
# run qemu|virt|test|watchdog|hw|auto build-and-run a board, the boot-watchdog, or attach to HW
# port check|diff|gates|pin Flash-port helpers (transpile, review-diff, gate battery)
# build two-pass kernel build (wraps ./build.sh)
# flashos list shell helpers + zig build steps
# Legacy flat names (piconnect/picapture/pilist/piquit) stay as thin aliases.
# Resolve the directory of this file once at source time. Inside a function,
# ${0:A:h} refers to the function name, not the source file — capture it now
# while %x still points at the file being sourced.
typeset -g _FLASHOS_DIR="${${(%):-%x}:A:h}"
typeset -g _RED=$'\033[0;31m'
typeset -g _GREEN=$'\033[0;32m'
typeset -g _YELLOW=$'\033[1;33m'
typeset -g _NC=$'\033[0m'
# Screen session name shared by `pi capture` (creates it) and `pi quit` (kills it).
typeset -g _FLASHOS_CAPTURE_SESSION="pi_capture"
# Console serial parameters and the device tables the helpers match on, keyed by
# logical name (usb|mu). Mini-UART trace rides a USB-serial adapter
# (/dev/cu.usbserial-*); the USB CDC console (fsh) enumerates as
# /dev/cu.usbmodem*. _GLOB = the match pattern, _OVERRIDE = an env-var honored
# verbatim when set, _LABEL = the human name used in messages. Adding a second
# adapter later is one aligned row per table.
typeset -g _FLASHOS_BAUD=115200
typeset -gA _FLASHOS_DEV_GLOB=( usb '/dev/cu.usbmodem*' mu '/dev/cu.usbserial-*' )
typeset -gA _FLASHOS_DEV_OVERRIDE=( usb PI_USB_CONSOLE_DEVICE mu PI_SERIAL_DEVICE )
typeset -gA _FLASHOS_DEV_LABEL=( usb 'usb console (fsh)' mu 'usb-serial adapter (MU trace)' )
# Default mount point for `zig build deploy`. Override per-shell with
# SD_BOOT=/Volumes/OTHER zig build deploy.
: "${SD_BOOT:=/Volumes/BOOT}"
export SD_BOOT
# ── shared primitives ──────────────────────────────────────────────────────
# One idiom each for diagnostics and for running argv in the project root, so
# every helper below speaks the same way: red/yellow go to stderr, green to
# stdout, and project commands run from $_FLASHOS_DIR in a subshell.
_flashos_err() { print -u2 -- "${_RED}$*${_NC}"; }
_flashos_warn() { print -u2 -- "${_YELLOW}$*${_NC}"; }
_flashos_ok() { print -- "${_GREEN}$*${_NC}"; }
_flashos_root() { ( cd "$_FLASHOS_DIR" && "$@" ); }
# Echo a console device path on stdout, or return non-zero with a message on
# stderr. $1 = NAME of an override env-var (honored verbatim if set), $2 = the
# glob to match otherwise, $3 = human label used in the not-found message.
_flashos_pick_device() {
emulate -L zsh
local override=${(P)1}
if [[ -n "$override" ]]; then
if [[ ! -e "$override" ]]; then
_flashos_err "\$$1 ($override) does not exist"
return 1
fi
print -r -- "$override"
return 0
fi
local devs=(${~2}(N))
if (( ${#devs} == 0 )); then
_flashos_err "no '$3' device found"
return 1
fi
(( ${#devs} > 1 )) && _flashos_warn "multiple '$3' devices match; using ${devs[1]}"
print -r -- "${devs[1]}"
}
# $1 = usb|mu. Echoes a console device path on stdout, or errors with the
# device's human label. The override env-vars (PI_USB_CONSOLE_DEVICE /
# PI_SERIAL_DEVICE) are honored verbatim; see the _FLASHOS_DEV_* tables above.
_flashos_device() {
emulate -L zsh
_flashos_pick_device "${_FLASHOS_DEV_OVERRIDE[$1]}" "${_FLASHOS_DEV_GLOB[$1]}" "${_FLASHOS_DEV_LABEL[$1]}"
}
# Named wrappers kept for readability at the call sites.
# _flashos_serial_device — Mini-UART trace adapter; honors $PI_SERIAL_DEVICE.
# _flashos_usb_console_device — USB CDC console (where fsh lives once the
# gadget enumerates); honors $PI_USB_CONSOLE_DEVICE.
_flashos_serial_device() { _flashos_device mu; }
_flashos_usb_console_device() { _flashos_device usb; }
# ── pi domain: serial-console helpers ──────────────────────────────────────
# Verbs are dispatched by pi() below; each impl is also reachable through its
# legacy flat alias (piquit/pilist/piconnect/picapture).
# Quit the capture screen session.
_flashos_pi_quit() {
emulate -L zsh
if screen -S "$_FLASHOS_CAPTURE_SESSION" -X quit 2>/dev/null; then
_flashos_ok "capture session terminated"
else
_flashos_warn "no capture session is running"
fi
}
# List attached console devices, iterating the device table so a new adapter
# row shows up here for free.
_flashos_pi_list() {
emulate -L zsh
local any=0 k devs
for k in usb mu; do
devs=(${~_FLASHOS_DEV_GLOB[$k]}(N))
(( ${#devs} == 0 )) && continue
any=1
print -- "${_FLASHOS_DEV_LABEL[$k]}:"
print -l -- " "${^devs}
done
(( any )) || { _flashos_err "no device found"; return 1; }
}
# Page the last boot.log capture (created by `pi capture`).
_flashos_pi_log() {
emulate -L zsh
local f="$_FLASHOS_DIR/boot.log"
if [[ ! -s "$f" ]]; then
_flashos_warn "no capture yet (run 'pi capture'): $f"
return 1
fi
${PAGER:-less} "$f"
}
# Live-tail the last boot.log. `-F` follows across the rm+recreate the next
# `pi capture` does, so it keeps working through a re-run.
# pi tail [N] show the last N lines (default 40), then follow
_flashos_pi_tail() {
emulate -L zsh
tail -n "${1:-40}" -F "$_FLASHOS_DIR/boot.log"
}
# Attach an interactive screen session to the Pi console.
# pi connect auto: USB CDC console (fsh) if present, else MU trace adapter
# pi connect usb force the USB CDC console (/dev/cu.usbmodem*)
# pi connect mu force the Mini-UART trace adapter (/dev/cu.usbserial-*)
# Once the gadget enumerates, fsh I/O rides USB; the MU adapter then only
# carries kernel [Debug] prints and the USB bring-up trace.
_flashos_pi_connect() {
emulate -L zsh
local mode="${1:-auto}"
local device
case "$mode" in
usb)
device="$(_flashos_usb_console_device)" || return 1
;;
mu)
device="$(_flashos_serial_device)" || return 1
;;
auto)
if device="$(_flashos_usb_console_device 2>/dev/null)"; then
_flashos_ok "usb console (fsh): ${device}"
elif device="$(_flashos_serial_device 2>/dev/null)"; then
_flashos_warn "mu trace adapter (${device}) — kernel [Debug] only; fsh rides usb once enumerated"
else
_flashos_err "no console device found (no ${_FLASHOS_DEV_GLOB[usb]} or ${_FLASHOS_DEV_GLOB[mu]})"
return 1
fi
;;
*)
_flashos_err "pi connect: unknown target '$mode' (usb|mu|auto)"
return 1
;;
esac
screen "$device" "$_FLASHOS_BAUD"
}
# ── pi capture: orchestrator + extracted stages ────────────────────────────
# `pi capture` watches the Pi console until boot success is confirmed, then
# closes the session. The log always lands at $_FLASHOS_DIR/boot.log (covered by
# the repo .gitignore), regardless of the current directory. The work is split
# so each stage is readable and the two poll loops are testable against a
# fixture logfile without a Pi or a screen session: the wait helpers echo a
# single result token on stdout and route progress/status to stderr.
# Wait for the USB CDC gadget to enumerate. Echoes the device path on stdout;
# the /dev/cu.usbmodem* node only exists once the gadget is up, so its
# appearance is itself the first boot signal.
_flashos_capture_wait_enumerate() {
emulate -L zsh
local timeout=${PI_CAPTURE_TIMEOUT:-120} waited=0 device
print -u2 -- "plug in the C-to-C cable now (powers the pi + carries the console)..."
print -nu2 -- "waiting for enumeration "
while ! device="$(_flashos_usb_console_device 2>/dev/null)"; do
sleep 1
(( waited++ ))
print -nu2 -- "."
if (( waited >= timeout )); then
_flashos_err "\ntimeout: no ${_FLASHOS_DEV_GLOB[usb]} appeared after ${timeout}s"
_flashos_warn "kernel faults only print on the mu adapter — try 'pi capture mu' with external power"
return 1
fi
done
_flashos_ok "\nenumerated (${device})" >&2
print -r -- "$device"
}
# Acquire the capture device for the given mode. Echoes the device path on
# stdout; all status goes to stderr so the caller can capture the path cleanly.
_flashos_capture_acquire() {
emulate -L zsh
local mode=$1 device
if [[ "$mode" == "mu" ]]; then
device="$(_flashos_serial_device)" || return 1
_flashos_ok "cable detected (${device})" >&2
print -u2 -- "please connect pi to power now..."
elif device="$(_flashos_usb_console_device 2>/dev/null)"; then
_flashos_ok "usb console already present (${device}) — pi appears to be running" >&2
else
device="$(_flashos_capture_wait_enumerate)" || return 1
fi
print -r -- "$device"
}
# Start a detached, logging screen session on $device. Returns 0 on success.
_flashos_capture_screen_start() {
emulate -L zsh
local session=$1 device=$2 logfile=$3 screenrc
screen -S "$session" -X quit >/dev/null 2>&1
# macOS ships screen 4.00.03, which has no -Logfile flag; the log filename
# comes from a screenrc `logfile` directive instead (plain -L would write
# screenlog.0). `logfile flush 1` flushes every 1s so the live grep in the
# wait loops sees the boot marker without screen's default 10s buffering.
screenrc="$(mktemp)" || { _flashos_err "mktemp failed"; return 1; }
print -r -- "logfile \"$logfile\"" > "$screenrc"
print -r -- "logfile flush 1" >> "$screenrc"
if ! screen -c "$screenrc" -L -dmS "$session" "$device" "$_FLASHOS_BAUD"; then
rm -f -- "$screenrc"
_flashos_err "failed to start screen session"
return 1
fi
sleep 1
if ! screen -list | grep -q "\.${session}[[:space:]]"; then
rm -f -- "$screenrc"
_flashos_err "screen session failed to start; port may be occupied"
return 1
fi
rm -f -- "$screenrc" # screen has read the rc; safe to remove now
}
# Mini-UART capture loop. Echoes one result token (success|error|failed|timeout)
# on stdout; progress dots go to stderr.
_flashos_capture_wait_mu() {
emulate -L zsh
local logfile=$1
local timeout=${PI_CAPTURE_TIMEOUT:-120} elapsed=0 result=timeout
print -nu2 -- "monitoring "
while (( elapsed < timeout )); do
sleep 1
(( elapsed++ ))
print -nu2 -- "."
[[ -f "$logfile" ]] || continue
# Check ERROR before success: if both appear, the error is the more
# informative outcome.
if grep -qF "ERROR CAUGHT:" "$logfile"; then
result=error
break
fi
if grep -qF "[FAIL]" "$logfile"; then
result=failed
break
fi
# Boot-complete depends on the build:
# * A self-test build runs the in-kernel suite, whose scripted login
# scenario prints `login:` TWICE mid-run -- so a bare `login:` match
# fires before the suite finishes and truncates the capture. Its real
# completion is the 3rd homescreen marker (`type 'help' for commands`;
# two scripted login sessions + the real boot login), the same count
# run_qemu_test.sh trusts.
# * A clean deploy/shipping kernel has no scripted test lines and never
# auto-logs-in, so it stops at the password-gated `login:` -- that
# prompt is its boot-complete signal (the homescreen marker never
# anchors there, so waiting on it would hang).
if grep -qF "[TEST]" "$logfile"; then
if [[ "$(grep -cF "type 'help' for commands" "$logfile")" -ge 3 ]]; then
result=success
break
fi
elif grep -qF "login:" "$logfile"; then
result=success
break
fi
done
print -r -- "$result"
}
# USB CDC capture loop. Echoes one result token (success|died|timeout) on
# stdout; progress dots go to stderr.
_flashos_capture_wait_usb() {
emulate -L zsh
local session=$1 logfile=$2
local timeout=${PI_PROBE_TIMEOUT:-30} elapsed=0 result=timeout
# Stuff a CR each second to wake/keep the session (readline submits on CR,
# an empty line is a no-op dispatch), and watch for the one-time boot marker
# `type 'help' for commands` — the same interactive-REPL signal run_qemu_test.sh
# and mu-mode trust. The shell prompt is `# ` / `$ `; it never prints `>>> `.
# `-p 0` is mandatory: a born-detached (-dmS) session has no current
# window on macOS screen 4.00.03, so -X stuff silently goes nowhere
# without an explicit window target.
print -nu2 -- "monitoring "
while (( elapsed < timeout )); do
screen -S "$session" -p 0 -X stuff $'\r' >/dev/null 2>&1
sleep 1
(( elapsed++ ))
print -nu2 -- "."
if ! screen -list 2>/dev/null | grep -q "\.${session}[[:space:]]"; then
result=died
break
fi
if [[ -f "$logfile" ]] && grep -qF "type 'help' for commands" "$logfile"; then
result=success
break
fi
done
print -r -- "$result"
}
# Report the capture outcome, tear the session down, and return 0 on success.
_flashos_capture_report() {
emulate -L zsh
local mode=$1 result=$2 logfile=$3
local timeout=${PI_CAPTURE_TIMEOUT:-120} probe_timeout=${PI_PROBE_TIMEOUT:-30}
case "$result" in
success)
_flashos_ok "\nboot successful"
;;
error)
_flashos_err "\nkernel fault: 'error caught' identified"
;;
failed)
_flashos_err "\nharness failure: a '[FAIL]' scenario was detected"
;;
died)
_flashos_err "\nscreen session died mid-capture — device disconnected or re-enumerated"
_flashos_warn "device re-enumerated: re-run pi capture to attach to the fresh node"
;;
timeout)
if [[ "$mode" == "mu" ]]; then
_flashos_warn "\ntimeout: no relevant kernel messages detected after ${timeout}s"
else
_flashos_warn "\ntimeout: enumerated, but fsh did not answer the prompt probe after ${probe_timeout}s"
_flashos_warn "kernel faults only print on the mu adapter — try 'pi capture mu' with external power"
fi
;;
esac
screen -S "$_FLASHOS_CAPTURE_SESSION" -X quit >/dev/null 2>&1
print -- "session terminated"
print -- "output saved to: $logfile"
# On a bad outcome, show the tail of the capture so the failure is visible
# without opening the log.
if [[ "$result" != "success" && -s "$logfile" ]]; then
print -- "last lines:"
tail -n 15 "$logfile"
fi
if [[ ! -s "$logfile" ]]; then
_flashos_warn "warning: $logfile is empty"
if [[ "$mode" == "mu" ]]; then
_flashos_warn "verify tx/rx wiring (pins 14/15) and power supply"
else
_flashos_warn "fsh may be wedged — power-cycle the pi and re-run"
fi
fi
[[ "$result" == "success" ]]
}
# Capture the Pi console until boot success is confirmed, then close the session.
# pi capture usb mode (default): wait for the CDC gadget to enumerate
# on /dev/cu.usbmodem*, then wait for the homescreen marker
# (`type 'help' for commands`)
# pi capture mu mini-UART mode: capture /dev/cu.usbserial-* until the
# harness prints its `N/N passed` tally (green; the shipping
# kernel then waits at the real `login:` prompt) or a
# `[FAIL]` / `ERROR CAUGHT:` appears
# Kernel faults always print on the MU adapter, never on USB — use mu mode
# (trace adapter + external non-host power) for fault diagnosis.
_flashos_pi_capture() {
emulate -L zsh
local mode="${1:-usb}"
case "$mode" in
usb|mu) ;;
*)
_flashos_err "pi capture: unknown target '$mode' (usb|mu)"
return 1
;;
esac
local logfile="$_FLASHOS_DIR/boot.log"
local session="$_FLASHOS_CAPTURE_SESSION"
local device result
device="$(_flashos_capture_acquire "$mode")" || return 1
rm -f -- "$logfile"
_flashos_capture_screen_start "$session" "$device" "$logfile" || return 1
if [[ "$mode" == "mu" ]]; then
result="$(_flashos_capture_wait_mu "$logfile")"
else
result="$(_flashos_capture_wait_usb "$session" "$logfile")"
fi
_flashos_capture_report "$mode" "$result" "$logfile"
}
# pi <verb> — dispatcher for the serial-console helpers above.
_flashos_pi_usage() {
print -- "usage: pi <verb> [args]"
print -- " connect [usb|mu|auto] attach an interactive screen session (default: auto)"
print -- " capture [usb|mu] capture boot.log until success/fault (default: usb)"
print -- " list list attached console devices"
print -- " log page the last boot.log capture"
print -- " tail [N] live-tail the last boot.log (default: 40 lines)"
print -- " quit kill the detached capture session"
}
pi() {
emulate -L zsh
local verb="${1:-help}"
(( $# > 0 )) && shift
case "$verb" in
connect) _flashos_pi_connect "$@" ;;
capture) _flashos_pi_capture "$@" ;;
list) _flashos_pi_list "$@" ;;
log) _flashos_pi_log "$@" ;;
tail) _flashos_pi_tail "$@" ;;
quit) _flashos_pi_quit "$@" ;;
help|-h|--help) _flashos_pi_usage ;;
*)
_flashos_err "pi: unknown verb '$verb'"
_flashos_pi_usage >&2
return 1
;;
esac
}
# Legacy flat names — kept so existing docs, scripts, and muscle memory keep
# working. Prefer the `pi <verb>` form; these forward to the same impls.
piconnect() { _flashos_pi_connect "$@"; }
picapture() { _flashos_pi_capture "$@"; }
pilist() { _flashos_pi_list "$@"; }
piquit() { _flashos_pi_quit "$@"; }
# ── run domain: build + emulate/run ────────────────────────────────────────
# Two-pass kernel build — thin wrapper around ./build.sh, anchored to the
# project root so it works from any directory. All build logic (zig version
# pin, nm passes, symbol diff, deploy prompt) lives in build.sh; keep it
# there so the two can never drift apart. NOTE: `build` runs the separate
# ./build.sh script (two passes); `run *` invokes the zig build system — they
# share a word but are different tools.
# build rpi4b build, deploy prompt at the end
# BOARD=virt build virt build (deploy skipped)
# NM=llvm-nm build override the nm binary
build() {
emulate -L zsh
_flashos_root ./build.sh "$@"
}
# run <mode> — build-and-run a board in QEMU, run the boot-watchdog, or attach
# to real hardware.
_flashos_run_usage() {
print -- "usage: run [qemu|virt|test|watchdog|hw|auto] [zig args...]"
print -- " qemu rpi4b board in QEMU (default via 'auto')"
print -- " virt qemu virt board (FROZEN 2026-06-17 — deprioritized; still builds)"
print -- " test host unit tests (--NAME filters by test name, e.g. run test --fat32)"
print -- " watchdog [virt|rpi4b] boot-watchdog; always seeds login + selftest (default: rpi4b)"
print -- " hw attach to the Raspberry Pi over serial (pi connect; --trace = MU adapter)"
print -- " auto alias for qemu"
}
run() {
emulate -L zsh
local mode="${1:-auto}"
(( $# > 0 )) && shift
case "$mode" in
qemu|auto) _flashos_root zig build -Dboard=rpi4b run "$@" ;;
virt) _flashos_root zig build -Dboard=virt run-virt "$@" ;;
test)
# A bare `--NAME` is sugar for the host-test substring filter
# (`run test --fat32` -> `zig build test -Dtest-filter=fat32`); -D… and
# other args pass through untouched, and `--help` reaches zig verbatim.
local -a targs=() a
for a in "$@"; do
case "$a" in
--help) targs+=(--help) ;;
--?*) targs+=("-Dtest-filter=${a#--}") ;;
*) targs+=("$a") ;;
esac
done
_flashos_root zig build test "${targs[@]}"
;;
watchdog|check)
# Boot-watchdog (test-virt / test-rpi4b). It hangs to its FULL timeout
# unless the kernel is built with BOTH -Dci-login-seed=true (auto-auth
# past the login: prompt) AND -Dboot-selftest=true (the in-kernel test
# scenarios the contract counts). Missing either flag rides the timeout —
# and on rpi4b that is ~12 min of TCG. Bake both in so
# the footgun cannot happen. Defaults to rpi4b (the live board); virt
# is frozen as of 2026-06-17 (deprioritized) but still an explicit opt-in.
local wb="${1:-rpi4b}" step
(( $# > 0 )) && shift
case "$wb" in
virt) step=test-virt
_flashos_warn "virt watchdog: board is FROZEN (deprioritized 2026-06-17); rpi4b + HW are the live gates" ;;
rpi4b) step=test-rpi4b
_flashos_warn "rpi4b watchdog: ~5-8 min of TCG (720s ceiling)" ;;
*) _flashos_err "run watchdog: unknown board '$wb' (virt|rpi4b)"; return 1 ;;
esac
_flashos_root zig build -Dboard="$wb" -Dci-login-seed=true -Dboot-selftest=true "$step" "$@"
local rc=$?
# run_qemu_test.sh is silent on success — QEMU output goes to a temp log it
# deletes on exit, and only a FAIL dumps the tail. Without an explicit
# verdict a green run is indistinguishable from a no-op (e.g. a cache
# skip), so print one keyed on the exit status.
if (( rc == 0 )); then
_flashos_ok "watchdog $wb: PASS — boot contract satisfied (rc=0)"
else
_flashos_err "watchdog $wb: FAIL (rc=$rc) — see the log tail above"
fi
return $rc
;;
hw)
# `--trace` (or `--Trace`) selects the Mini-UART adapter that carries the
# bring-up trace; everything else forwards to `pi connect` (usb|mu|auto).
local -a hargs=() a
for a in "$@"; do
case "$a" in
--trace|--Trace) hargs+=(mu) ;;
*) hargs+=("$a") ;;
esac
done
_flashos_pi_connect "${hargs[@]:-auto}"
;;
help|-h|--help) _flashos_run_usage ;;
*)
_flashos_err "run: unknown mode '$mode'"
_flashos_run_usage >&2
return 1
;;
esac
}
# ── flash port ──────────────────────────────────────────────────────────────
# Helpers for porting modules to Flash (*.flash transpiled to Zig at build
# time). A module counts as ported only when it transpiles clean, its
# generated Zig review-diffs against the original down to mechanical lowering
# noise, and the full gate battery is green. The flashc binary resolves like
# build.zig's -Dflashc default: $FLASHC if set, else
# $HOME/Flash/zig-out/bin/flashc-stage1 (override the checkout location with
# $FLASH_DIR). The pinned compiler revision lives in flash-toolchain.lock.
_flashos_flashc() {
local bin="${FLASHC:-${FLASH_DIR:-$HOME/Flash}/zig-out/bin/flashc-stage1}"
if [[ ! -x "$bin" ]]; then
_flashos_err "flashc not found: $bin (set \$FLASHC, or build the Flash checkout with 'zig build')"
return 1
fi
print -- "$bin"
}
_flashos_port_check() {
local src="$1"
[[ -n "$src" ]] || { _flashos_err "usage: port check <module.flash>"; return 1; }
local bin; bin="$(_flashos_flashc)" || return 1
local tmpd; tmpd="$(mktemp -d "${TMPDIR:-/tmp}/flashport.XXXXXX")" || return 1
if "$bin" "$src" -o "$tmpd/out.zig"; then
_flashos_ok "transpile clean: $src"
rm -rf "$tmpd"
else
rm -rf "$tmpd"
return 1
fi
}
_flashos_port_diff() {
local src="$1" orig="${2:-}"
[[ -n "$src" ]] || { _flashos_err "usage: port diff <module.flash> [original.zig]"; return 1; }
# Default original: same directory, same stem, .zig extension. The pilot
# already breaks that guess (hello.flash vs hello_elf.zig), so the second
# arg stays first-class.
[[ -n "$orig" ]] || orig="${src:r}.zig"
if [[ ! -f "$orig" ]]; then
_flashos_err "port diff: original not found: $orig (pass it explicitly)"
return 1
fi
local bin; bin="$(_flashos_flashc)" || return 1
local tmpd; tmpd="$(mktemp -d "${TMPDIR:-/tmp}/flashport.XXXXXX")" || return 1
local gen="$tmpd/${src:t:r}.zig"
if ! "$bin" "$src" -o "$gen"; then
rm -rf "$tmpd"
return 1
fi
# Lowering drops comments, so hunks are expected; the review bar is that
# every hunk is mechanical (comments, formatting), never semantic.
if diff -u "$orig" "$gen"; then
_flashos_ok "port diff: generated Zig is identical to $orig"
else
_flashos_warn "port diff: review the hunks above — only mechanical lowering differences are acceptable"
fi
rm -rf "$tmpd"
}
_flashos_port_gates() {
# The per-module gate battery: host tests, then the boot watchdog(s).
# Fail-fast — a red gate stops the run so the log tail is the culprit's.
local board="${1:-all}"
case "$board" in
virt|rpi4b|all) ;;
*) _flashos_err "port gates: unknown board '$board' (virt|rpi4b|all)"; return 1 ;;
esac
run test || { _flashos_err "port gates: host tests FAILED"; return 1; }
if [[ "$board" == virt || "$board" == all ]]; then
run watchdog virt || return 1
fi
if [[ "$board" == rpi4b || "$board" == all ]]; then
run watchdog rpi4b || return 1
fi
_flashos_ok "port gates: all green ($board)"
}
_flashos_port_pin() {
# Compare flash-toolchain.lock against the live Flash checkout. The port
# must never ride a moving compiler: drift means rebuild at the pin, or
# bump the lock as its own deliberate commit.
local lock="$_FLASHOS_DIR/flash-toolchain.lock"
[[ -f "$lock" ]] || { _flashos_err "port pin: no flash-toolchain.lock in the tree (not on the port branch?)"; return 1; }
local flash_dir="${FLASH_DIR:-$HOME/Flash}"
local pinned live
pinned="$(grep -E '^flash-commit' "$lock" | sed 's/.*= *//')"
live="$(git -C "$flash_dir" rev-parse HEAD 2>/dev/null)" || {
_flashos_err "port pin: no Flash checkout at $flash_dir (set \$FLASH_DIR)"
return 1
}
if [[ "$pinned" != "$live" ]]; then
_flashos_warn "port pin: DRIFT — lock $pinned, live $live"
_flashos_warn " rebuild the Flash checkout at the pin, or bump the lock in its own commit"
return 1
fi
if [[ -n "$(git -C "$flash_dir" status --porcelain 2>/dev/null)" ]]; then
_flashos_warn "port pin: commit matches but the Flash tree is DIRTY — flashc may not match the pin"
return 1
fi
_flashos_ok "port pin: OK ($pinned)"
}
_flashos_port_usage() {
print -- "usage: port [check|diff|gates|pin]"
print -- " check <module.flash> transpile-only; show flashc diagnostics"
print -- " diff <module.flash> [orig.zig]"
print -- " transpile and diff the generated Zig against"
print -- " the original (default: <stem>.zig next to it)"
print -- " gates [virt|rpi4b|all] per-module gate battery: host tests, then the"
print -- " boot watchdog(s); fail-fast (default: all)"
print -- " pin verify flash-toolchain.lock against the live"
print -- " Flash checkout (\$FLASH_DIR, default ~/Flash)"
}
port() {
emulate -L zsh
local verb="${1:-help}"
(( $# > 0 )) && shift
case "$verb" in
check) _flashos_port_check "$@" ;;
diff) _flashos_port_diff "$@" ;;
gates) _flashos_port_gates "$@" ;;
pin) _flashos_port_pin "$@" ;;
help|-h|--help) _flashos_port_usage ;;
*)
_flashos_err "port: unknown verb '$verb'"
_flashos_port_usage >&2
return 1
;;
esac
}
# ── introspection ──────────────────────────────────────────────────────────
# List the public shell helpers and the zig build steps. Named `flashos` rather
# than `help` so sourcing this file does not clobber a user's `help`/`run-help`.
flashos() {
emulate -L zsh
local project_file="$_FLASHOS_DIR/flashos.env.zsh"
local zig_file="$_FLASHOS_DIR/build.zig"
# Only the public surface — internal _flashos_* helpers start with '_'.
print -- "--- shell functions (flashos.env.zsh) ---"
if [[ -f "$project_file" ]]; then
grep -E '^[[:alpha:]][[:alnum:]_-]*\(\)' "$project_file" | sed 's/().*//'
else
_flashos_err "not found: $project_file"
fi
print -- "\n--- zig build steps (build.zig) ---"
if [[ -f "$zig_file" ]]; then
_flashos_root zig build --list-steps 2>/dev/null
else
_flashos_err "not found: $zig_file"
fi
}
# ── completion ──────────────────────────────────────────────────────────────
# zsh tab-completion for the pi/run verb dispatchers. compinit runs from
# ~/.zshrc before this file is sourced, so compdef is defined by the time we
# get here; the $+functions guard keeps the file safe to source in a shell
# where it is not.
_flashos_pi_completion() {
local -a verbs=(
'connect:attach an interactive screen session'
'capture:capture boot.log until success/fault'
'list:list attached console devices'
'log:page the last boot.log capture'
'tail:live-tail the last boot.log'
'quit:kill the detached capture session'
'help:usage'
)
if (( CURRENT == 2 )); then
_describe -t verbs 'pi verb' verbs
elif (( CURRENT == 3 )); then
case "${words[2]}" in
connect) _values 'target' usb mu auto ;;
capture) _values 'mode' usb mu ;;
esac
fi
}
_flashos_run_completion() {
local -a modes=(
'qemu:rpi4b board in QEMU'
'virt:qemu virt board'
'test:host unit tests (--NAME filters)'
'watchdog:boot-watchdog (seeds login + selftest)'
'hw:attach to the Pi over serial'
'auto:alias for qemu'
'help:usage'
)
if (( CURRENT == 2 )); then
_describe -t modes 'run mode' modes
elif (( CURRENT == 3 )); then
case "${words[2]}" in
watchdog|check) _values 'board' virt rpi4b ;;
hw) _values 'target' usb mu auto --trace ;;
esac
fi
}
_flashos_port_completion() {
local -a verbs=(
'check:transpile-only, show flashc diagnostics'
'diff:diff generated Zig against the original'
'gates:host tests + boot watchdog(s), fail-fast'
'pin:verify flash-toolchain.lock against the live checkout'
'help:usage'
)
if (( CURRENT == 2 )); then
_describe -t verbs 'port verb' verbs
elif (( CURRENT == 3 )); then
case "${words[2]}" in
check|diff) _files -g '*.flash' ;;
gates) _values 'board' virt rpi4b all ;;
esac
elif (( CURRENT == 4 )); then
case "${words[2]}" in
diff) _files -g '*.zig' ;;
esac
fi
}
if (( $+functions[compdef] )); then
compdef _flashos_pi_completion pi
compdef _flashos_run_completion run
compdef _flashos_port_completion port
fi