diff --git a/docs/user-guide.md b/docs/user-guide.md
index 3b3c4592..f2e82195 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -964,24 +964,32 @@ This is useful when:
**Install QEMU (one-time setup):**
```bash
-# Option 1: Build from source (recommended)
-git clone https://github.com/espressif/qemu.git
-cd qemu
-./configure --target-list=xtensa-softmmu
-make -j$(nproc)
-# Add to your PATH, or set QEMU_PATH later:
-export QEMU_PATH=/path/to/qemu/build/qemu-system-xtensa
+# Easiest: use the automated installer (installs QEMU + Python tools)
+bash scripts/install-qemu.sh
-# Option 2: On some Linux distros
-sudo apt install qemu-system-misc
+# Or check what's already installed:
+bash scripts/install-qemu.sh --check
```
-**Install Python tools:**
+The installer detects your OS (Ubuntu, Fedora, macOS, etc.), installs build dependencies, clones Espressif's QEMU fork, builds it, and adds it to your PATH. It also installs the Python tools (`esptool`, `pyyaml`, `esp-idf-nvs-partition-gen`).
+
+
+Manual installation (if you prefer)
```bash
-pip install esptool esp-idf-nvs-partition-gen
+# Build from source
+git clone https://github.com/espressif/qemu.git
+cd qemu
+./configure --target-list=xtensa-softmmu --enable-slirp
+make -j$(nproc)
+export QEMU_PATH=$(pwd)/build/qemu-system-xtensa
+
+# Install Python tools
+pip install esptool pyyaml esp-idf-nvs-partition-gen
```
+
+
**For multi-node testing (optional):**
```bash
@@ -989,16 +997,35 @@ pip install esptool esp-idf-nvs-partition-gen
sudo apt install socat bridge-utils iproute2
```
+### The `qemu-cli.sh` Command
+
+All QEMU testing is available through a single command:
+
+```bash
+bash scripts/qemu-cli.sh
+```
+
+| Command | What it does |
+|---------|-------------|
+| `install` | Install QEMU (runs the installer above) |
+| `test` | Run single-node firmware test |
+| `swarm --preset smoke` | Quick 2-node swarm test |
+| `swarm --preset standard` | Standard 3-node test |
+| `mesh 3` | Multi-node mesh test |
+| `chaos` | Fault injection resilience test |
+| `fuzz --duration 60` | Run fuzz testing |
+| `status` | Show what's installed and ready |
+| `help` | Show all commands |
+
### Your First Test Run
The simplest way to test the firmware:
```bash
-# This one command does everything:
-# 1. Builds the firmware with fake WiFi data
-# 2. Creates a virtual flash drive
-# 3. Boots it in the emulator
-# 4. Checks the output for errors
+# Using the CLI:
+bash scripts/qemu-cli.sh test
+
+# Or directly:
bash scripts/qemu-esp32s3-test.sh
```
diff --git a/scripts/install-qemu.sh b/scripts/install-qemu.sh
new file mode 100644
index 00000000..adfe9842
--- /dev/null
+++ b/scripts/install-qemu.sh
@@ -0,0 +1,328 @@
+#!/bin/bash
+# install-qemu.sh — Install QEMU with ESP32-S3 support (Espressif fork)
+# Usage: bash scripts/install-qemu.sh [OPTIONS]
+set -euo pipefail
+
+# ── Colors ────────────────────────────────────────────────────────────────────
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
+BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
+
+info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+err() { echo -e "${RED}[ERROR]${NC} $*"; }
+step() { echo -e "\n${CYAN}${BOLD}▶ $*${NC}"; }
+
+# ── Defaults ──────────────────────────────────────────────────────────────────
+INSTALL_DIR="$HOME/.espressif/qemu"
+BRANCH="esp-develop"
+JOBS=""
+SKIP_DEPS=false
+UNINSTALL=false
+CHECK_ONLY=false
+QEMU_REPO="https://github.com/espressif/qemu.git"
+
+# ── Usage ─────────────────────────────────────────────────────────────────────
+usage() {
+ cat </dev/null; then
+ IS_WSL=true
+ fi
+ if [ -f /etc/os-release ]; then
+ # shellcheck disable=SC1091
+ . /etc/os-release
+ case "$ID" in
+ ubuntu|debian|pop|linuxmint|elementary) DISTRO="debian" ;;
+ fedora|rhel|centos|rocky|alma) DISTRO="fedora" ;;
+ arch|manjaro|endeavouros) DISTRO="arch" ;;
+ opensuse*|sles) DISTRO="suse" ;;
+ *) DISTRO="$ID" ;;
+ esac
+ fi
+ ;;
+ Darwin) OS="macos"; DISTRO="macos" ;;
+ *) err "Unsupported OS: $(uname -s)"; exit 3 ;;
+ esac
+
+ info "Detected: OS=${OS} Distro=${DISTRO} WSL=${IS_WSL}"
+}
+
+# ── Check existing installation ───────────────────────────────────────────────
+check_installation() {
+ local qemu_bin="$INSTALL_DIR/build/qemu-system-xtensa"
+ if [ -x "$qemu_bin" ]; then
+ local version
+ version=$("$qemu_bin" --version 2>/dev/null | head -1) || true
+ if [ -n "$version" ]; then
+ ok "QEMU installed: $version"
+ ok "Binary: $qemu_bin"
+ return 0
+ fi
+ fi
+ # Check PATH
+ if command -v qemu-system-xtensa &>/dev/null; then
+ local version
+ version=$(qemu-system-xtensa --version 2>/dev/null | head -1) || true
+ ok "QEMU found in PATH: $version"
+ return 0
+ fi
+ warn "QEMU with ESP32-S3 support not found"
+ return 1
+}
+
+if $CHECK_ONLY; then
+ detect_os
+ if check_installation; then exit 0; else exit 1; fi
+fi
+
+# ── Uninstall ─────────────────────────────────────────────────────────────────
+if $UNINSTALL; then
+ step "Uninstalling QEMU from $INSTALL_DIR"
+ if [ -d "$INSTALL_DIR" ]; then
+ rm -rf "$INSTALL_DIR"
+ ok "Removed $INSTALL_DIR"
+ else
+ warn "Directory not found: $INSTALL_DIR"
+ fi
+ # Remove symlink
+ local_bin="$HOME/.local/bin/qemu-system-xtensa"
+ if [ -L "$local_bin" ]; then
+ rm -f "$local_bin"
+ ok "Removed symlink $local_bin"
+ fi
+ ok "Uninstall complete"
+ exit 0
+fi
+
+# ── Main install flow ─────────────────────────────────────────────────────────
+detect_os
+
+# Default jobs = nproc
+if [ -z "$JOBS" ]; then
+ if command -v nproc &>/dev/null; then
+ JOBS=$(nproc)
+ elif command -v sysctl &>/dev/null; then
+ JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
+ else
+ JOBS=4
+ fi
+fi
+info "Build parallelism: $JOBS jobs"
+
+# ── Step 1: Install dependencies ──────────────────────────────────────────────
+install_deps() {
+ step "Installing build dependencies"
+
+ case "$DISTRO" in
+ debian)
+ info "Using apt (Debian/Ubuntu)"
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq \
+ git build-essential python3 python3-pip python3-venv \
+ ninja-build pkg-config libglib2.0-dev libpixman-1-dev \
+ libslirp-dev libgcrypt-dev
+ ;;
+ fedora)
+ info "Using dnf (Fedora/RHEL)"
+ sudo dnf install -y \
+ git gcc gcc-c++ make python3 python3-pip \
+ ninja-build pkgconfig glib2-devel pixman-devel \
+ libslirp-devel libgcrypt-devel
+ ;;
+ arch)
+ info "Using pacman (Arch)"
+ sudo pacman -S --needed --noconfirm \
+ git base-devel python python-pip \
+ ninja pkgconf glib2 pixman libslirp libgcrypt
+ ;;
+ suse)
+ info "Using zypper (openSUSE)"
+ sudo zypper install -y \
+ git gcc gcc-c++ make python3 python3-pip \
+ ninja pkg-config glib2-devel libpixman-1-0-devel \
+ libslirp-devel libgcrypt-devel
+ ;;
+ macos)
+ info "Using Homebrew"
+ if ! command -v brew &>/dev/null; then
+ err "Homebrew not found. Install from https://brew.sh"
+ exit 1
+ fi
+ brew install glib pixman ninja pkg-config libslirp libgcrypt || true
+ ;;
+ *)
+ warn "Unknown distro '$DISTRO' — install these manually:"
+ warn " git, gcc/g++, python3, ninja, pkg-config, glib2-dev, pixman-dev, libslirp-dev"
+ return 1
+ ;;
+ esac
+ ok "Dependencies installed"
+}
+
+if ! $SKIP_DEPS; then
+ install_deps || { err "Dependency installation failed"; exit 1; }
+else
+ info "Skipping dependency installation (--skip-deps)"
+fi
+
+# ── Step 2: Clone Espressif QEMU fork ─────────────────────────────────────────
+step "Cloning Espressif QEMU fork"
+
+SRC_DIR="$INSTALL_DIR"
+if [ -d "$SRC_DIR/.git" ]; then
+ info "Repository already exists at $SRC_DIR"
+ info "Fetching latest changes on branch $BRANCH"
+ git -C "$SRC_DIR" fetch origin "$BRANCH" --depth=1
+ git -C "$SRC_DIR" checkout "$BRANCH" 2>/dev/null || git -C "$SRC_DIR" checkout "origin/$BRANCH"
+ ok "Updated to latest $BRANCH"
+else
+ info "Cloning $QEMU_REPO (branch: $BRANCH)"
+ mkdir -p "$(dirname "$SRC_DIR")"
+ git clone --depth=1 --branch "$BRANCH" "$QEMU_REPO" "$SRC_DIR"
+ ok "Cloned to $SRC_DIR"
+fi
+
+# ── Step 3: Configure and build ───────────────────────────────────────────────
+step "Configuring QEMU (target: xtensa-softmmu)"
+
+BUILD_DIR="$SRC_DIR/build"
+mkdir -p "$BUILD_DIR"
+cd "$SRC_DIR"
+
+./configure \
+ --target-list=xtensa-softmmu \
+ --enable-slirp \
+ --enable-gcrypt \
+ --prefix="$INSTALL_DIR/dist" \
+ 2>&1 | tail -5
+
+step "Building QEMU ($JOBS parallel jobs)"
+make -j"$JOBS" -C "$BUILD_DIR" 2>&1 | tail -20
+
+if [ ! -x "$BUILD_DIR/qemu-system-xtensa" ]; then
+ err "Build failed — qemu-system-xtensa binary not found"
+ err "Troubleshooting:"
+ err " 1. Check build output above for errors"
+ err " 2. Ensure all dependencies are installed: re-run without --skip-deps"
+ err " 3. Try with fewer jobs: --jobs 1"
+ err " 4. On macOS, ensure Xcode CLT: xcode-select --install"
+ exit 2
+fi
+ok "Build succeeded: $BUILD_DIR/qemu-system-xtensa"
+
+# ── Step 4: Create symlink / add to PATH ──────────────────────────────────────
+step "Setting up PATH access"
+
+LOCAL_BIN="$HOME/.local/bin"
+mkdir -p "$LOCAL_BIN"
+ln -sf "$BUILD_DIR/qemu-system-xtensa" "$LOCAL_BIN/qemu-system-xtensa"
+ok "Symlinked to $LOCAL_BIN/qemu-system-xtensa"
+
+# Check if ~/.local/bin is in PATH
+if ! echo "$PATH" | tr ':' '\n' | grep -qx "$LOCAL_BIN"; then
+ warn "$LOCAL_BIN is not in your PATH"
+ warn "Add this to your shell profile (~/.bashrc or ~/.zshrc):"
+ echo -e " ${BOLD}export PATH=\"\$HOME/.local/bin:\$PATH\"${NC}"
+fi
+
+# ── Step 5: Verify ────────────────────────────────────────────────────────────
+step "Verifying installation"
+
+QEMU_VERSION=$("$BUILD_DIR/qemu-system-xtensa" --version | head -1)
+ok "$QEMU_VERSION"
+
+# Check ESP32-S3 machine support
+if "$BUILD_DIR/qemu-system-xtensa" -machine help 2>/dev/null | grep -q esp32s3; then
+ ok "ESP32-S3 machine type available"
+else
+ warn "ESP32-S3 machine type not listed (may still work with newer builds)"
+fi
+
+# ── Step 6: Install Python packages ──────────────────────────────────────────
+step "Installing Python packages (esptool, pyyaml, nvs-partition-gen)"
+
+PIP_CMD="pip3"
+if ! command -v pip3 &>/dev/null; then
+ PIP_CMD="python3 -m pip"
+fi
+
+$PIP_CMD install --user --quiet \
+ esptool \
+ pyyaml \
+ esp-idf-nvs-partition-gen \
+ 2>&1 || warn "Some Python packages failed to install (non-fatal)"
+
+ok "Python packages installed"
+
+# ── Done ──────────────────────────────────────────────────────────────────────
+echo ""
+echo -e "${GREEN}${BOLD}Installation complete!${NC}"
+echo ""
+echo -e "${BOLD}Next steps:${NC}"
+echo ""
+echo " 1. Run a smoke test:"
+echo -e " ${CYAN}qemu-system-xtensa -nographic -machine esp32s3 \\${NC}"
+echo -e " ${CYAN} -drive file=firmware.bin,if=mtd,format=raw \\${NC}"
+echo -e " ${CYAN} -serial mon:stdio${NC}"
+echo ""
+echo " 2. Run the project QEMU tests:"
+echo -e " ${CYAN}cd $(dirname "$0")/.."
+echo -e " pytest firmware/esp32-csi-node/tests/qemu/ -v${NC}"
+echo ""
+echo " 3. Binary location:"
+echo -e " ${CYAN}$BUILD_DIR/qemu-system-xtensa${NC}"
+echo ""
+echo -e " 4. Uninstall:"
+echo -e " ${CYAN}bash scripts/install-qemu.sh --uninstall${NC}"
+echo ""
diff --git a/scripts/qemu-cli.sh b/scripts/qemu-cli.sh
new file mode 100644
index 00000000..43ac3900
--- /dev/null
+++ b/scripts/qemu-cli.sh
@@ -0,0 +1,362 @@
+#!/usr/bin/env bash
+# ============================================================================
+# qemu-cli.sh — Unified QEMU ESP32-S3 testing CLI (ADR-061)
+# Version: 1.0.0
+#
+# Single entry point for all QEMU testing operations.
+# Run `qemu-cli.sh help` or `qemu-cli.sh --help` for usage.
+# ============================================================================
+set -euo pipefail
+
+VERSION="1.0.0"
+
+# --- Colors ----------------------------------------------------------------
+if [[ -t 1 ]]; then
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
+ BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RST='\033[0m'
+else
+ RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; RST=''
+fi
+
+# --- Resolve paths ---------------------------------------------------------
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+FIRMWARE_DIR="$PROJECT_ROOT/firmware/esp32-csi-node"
+FUZZ_DIR="$FIRMWARE_DIR/test"
+
+# --- Helpers ---------------------------------------------------------------
+info() { echo -e "${BLUE}[INFO]${RST} $*"; }
+ok() { echo -e "${GREEN}[OK]${RST} $*"; }
+warn() { echo -e "${YELLOW}[WARN]${RST} $*"; }
+err() { echo -e "${RED}[ERROR]${RST} $*" >&2; }
+die() { err "$@"; exit 1; }
+
+need_qemu() {
+ detect_qemu >/dev/null 2>&1 || \
+ die "QEMU not found. Install with: ${CYAN}qemu-cli.sh install${RST}"
+}
+
+detect_qemu() {
+ # 1. Explicit env var
+ if [[ -n "${QEMU_PATH:-}" ]] && [[ -x "$QEMU_PATH" ]]; then
+ echo "$QEMU_PATH"; return 0
+ fi
+ # 2. On PATH
+ local qemu
+ qemu="$(command -v qemu-system-xtensa 2>/dev/null || true)"
+ if [[ -n "$qemu" ]]; then echo "$qemu"; return 0; fi
+ # 3. Espressif default build location
+ local espressif_qemu="$HOME/.espressif/qemu/build/qemu-system-xtensa"
+ if [[ -x "$espressif_qemu" ]]; then echo "$espressif_qemu"; return 0; fi
+ return 1
+}
+
+detect_python() {
+ command -v python3 2>/dev/null || command -v python 2>/dev/null || echo "python3"
+}
+
+# --- Command: help ---------------------------------------------------------
+cmd_help() {
+ cat < [options]
+
+${BOLD}COMMANDS${RST}
+ ${CYAN}install${RST} Install QEMU with ESP32-S3 support
+ ${CYAN}test${RST} Run single-node firmware test
+ ${CYAN}mesh${RST} [N] Run multi-node mesh test (default: 3 nodes)
+ ${CYAN}swarm${RST} [args] Run swarm configurator (qemu_swarm.py)
+ ${CYAN}snapshot${RST} [args] Run snapshot-based tests
+ ${CYAN}chaos${RST} [args] Run chaos / fault injection tests
+ ${CYAN}fuzz${RST} [--duration N] Run all 3 fuzz targets (clang libFuzzer)
+ ${CYAN}nvs${RST} [args] Generate NVS test matrix
+ ${CYAN}health${RST} Check firmware health from QEMU log
+ ${CYAN}status${RST} Show installation status and versions
+ ${CYAN}help${RST} Show this help message
+
+${BOLD}EXAMPLES${RST}
+ qemu-cli.sh install # Install QEMU
+ qemu-cli.sh test # Run basic firmware test
+ qemu-cli.sh test --timeout 120 # Test with longer timeout
+ qemu-cli.sh swarm --preset smoke # Quick swarm test
+ qemu-cli.sh swarm --preset standard # Standard 3-node test
+ qemu-cli.sh swarm --list-presets # List available presets
+ qemu-cli.sh mesh 3 # 3-node mesh test
+ qemu-cli.sh chaos # Run chaos tests
+ qemu-cli.sh fuzz --duration 60 # Fuzz for 60 seconds
+ qemu-cli.sh nvs --list # List NVS configs
+ qemu-cli.sh health build/qemu_output.log
+ qemu-cli.sh status # Show what's installed
+
+${BOLD}TAB COMPLETION${RST}
+ Source the completions in your shell:
+ eval "\$(qemu-cli.sh --completions)"
+
+${BOLD}ENVIRONMENT${RST}
+ QEMU_PATH Path to qemu-system-xtensa binary (auto-detected)
+ FUZZ_DURATION Override fuzz duration in seconds (default: 30)
+ FUZZ_JOBS Parallel fuzzing jobs (default: 1)
+
+EOF
+}
+
+# --- Command: install ------------------------------------------------------
+cmd_install() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh install"
+ echo "Install QEMU with Espressif ESP32-S3 support."
+ return 0
+ fi
+ local installer="$SCRIPT_DIR/install-qemu.sh"
+ if [[ -f "$installer" ]]; then
+ info "Running install-qemu.sh ..."
+ bash "$installer" "$@"
+ else
+ info "No install-qemu.sh found. Showing manual install steps."
+ cat </dev/null || true
+ info "Running ${nodes}-node mesh test ..."
+ bash "$SCRIPT_DIR/qemu-mesh-test.sh" "$nodes" "$@"
+}
+
+# --- Command: swarm ---------------------------------------------------------
+cmd_swarm() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh swarm [--preset NAME] [--list-presets] [args...]"
+ echo "Run QEMU swarm configurator (qemu_swarm.py)."
+ echo ""
+ echo "Presets: smoke, standard, full, stress"
+ echo "List: qemu-cli.sh swarm --list-presets"
+ return 0
+ fi
+ need_qemu
+ local py; py="$(detect_python)"
+ info "Running swarm configurator ..."
+ "$py" "$SCRIPT_DIR/qemu_swarm.py" "$@"
+}
+
+# --- Command: snapshot ------------------------------------------------------
+cmd_snapshot() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh snapshot [args...]"
+ echo "Run snapshot-based QEMU tests."
+ return 0
+ fi
+ need_qemu
+ info "Running snapshot tests ..."
+ bash "$SCRIPT_DIR/qemu-snapshot-test.sh" "$@"
+}
+
+# --- Command: chaos ---------------------------------------------------------
+cmd_chaos() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh chaos [args...]"
+ echo "Run chaos / fault injection tests."
+ return 0
+ fi
+ need_qemu
+ info "Running chaos tests ..."
+ bash "$SCRIPT_DIR/qemu-chaos-test.sh" "$@"
+}
+
+# --- Command: fuzz ----------------------------------------------------------
+cmd_fuzz() {
+ local duration="${FUZZ_DURATION:-30}"
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh fuzz [--duration N]"
+ echo "Build and run all 3 fuzz targets (clang libFuzzer)."
+ echo "Requires: clang with libFuzzer support."
+ return 0
+ fi
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --duration) duration="$2"; shift 2 ;;
+ *) warn "Unknown fuzz option: $1"; shift ;;
+ esac
+ done
+ if ! command -v clang >/dev/null 2>&1; then
+ die "clang not found. Fuzz targets require clang with libFuzzer."
+ fi
+ info "Building and running fuzz targets (${duration}s each) ..."
+ make -C "$FUZZ_DIR" run_all FUZZ_DURATION="$duration"
+ ok "Fuzz testing complete."
+}
+
+# --- Command: nvs -----------------------------------------------------------
+cmd_nvs() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh nvs [--list] [args...]"
+ echo "Generate NVS test configuration matrix."
+ return 0
+ fi
+ local py; py="$(detect_python)"
+ info "Running NVS matrix generator ..."
+ "$py" "$SCRIPT_DIR/generate_nvs_matrix.py" "$@"
+}
+
+# --- Command: health --------------------------------------------------------
+cmd_health() {
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ echo "Usage: qemu-cli.sh health "
+ echo "Analyze firmware health from a QEMU output log."
+ return 0
+ fi
+ local logfile="${1:-}"
+ if [[ -z "$logfile" ]]; then
+ die "Usage: qemu-cli.sh health "
+ fi
+ if [[ ! -f "$logfile" ]]; then
+ die "Log file not found: $logfile"
+ fi
+ local py; py="$(detect_python)"
+ info "Analyzing health from: $logfile"
+ "$py" "$SCRIPT_DIR/check_health.py" --log "$logfile" --after-fault manual
+}
+
+# --- Command: status --------------------------------------------------------
+cmd_status() {
+ # Status should never fail — disable errexit locally
+ set +e
+ echo -e "${BOLD}=== QEMU ESP32-S3 Testing Status ===${RST}"
+ echo ""
+
+ # QEMU
+ local qemu_bin
+ qemu_bin="$(detect_qemu 2>/dev/null)"
+ if [[ -n "$qemu_bin" ]]; then
+ local qemu_ver
+ qemu_ver="$("$qemu_bin" --version 2>/dev/null | head -1 || echo "unknown")"
+ ok "QEMU: ${GREEN}installed${RST} ($qemu_ver)"
+ echo " Path: $qemu_bin"
+ else
+ warn "QEMU: ${YELLOW}not found${RST} (run: qemu-cli.sh install)"
+ fi
+
+ # ESP-IDF
+ if [[ -n "${IDF_PATH:-}" ]] && [[ -d "$IDF_PATH" ]]; then
+ ok "ESP-IDF: ${GREEN}available${RST} ($IDF_PATH)"
+ else
+ warn "ESP-IDF: ${YELLOW}IDF_PATH not set${RST}"
+ fi
+
+ # Python
+ local py; py="$(detect_python)"
+ if command -v "$py" >/dev/null 2>&1; then
+ ok "Python: ${GREEN}$("$py" --version 2>&1)${RST}"
+ else
+ warn "Python: ${YELLOW}not found${RST}"
+ fi
+
+ # Clang (for fuzz)
+ if command -v clang >/dev/null 2>&1; then
+ ok "Clang: ${GREEN}$(clang --version 2>/dev/null | head -1)${RST}"
+ else
+ warn "Clang: ${YELLOW}not found${RST} (needed for fuzz targets only)"
+ fi
+
+ # Firmware binary
+ local fw_bin="$FIRMWARE_DIR/build/esp32-csi-node.bin"
+ if [[ -f "$fw_bin" ]]; then
+ local fw_size
+ fw_size="$(stat -c%s "$fw_bin" 2>/dev/null || stat -f%z "$fw_bin" 2>/dev/null || echo "?")"
+ ok "Firmware: ${GREEN}built${RST} ($fw_bin, ${fw_size} bytes)"
+ else
+ warn "Firmware: ${YELLOW}not built${RST} (expected at $fw_bin)"
+ fi
+
+ # Swarm presets
+ local preset_dir="$SCRIPT_DIR/swarm_presets"
+ if [[ -d "$preset_dir" ]]; then
+ local presets
+ presets="$(ls "$preset_dir"/ 2>/dev/null | \
+ sed 's/\.\(yaml\|json\)$//' | sort -u | tr '\n' ', ' | sed 's/,$//')"
+ if [[ -n "$presets" ]]; then
+ ok "Presets: ${GREEN}${presets}${RST}"
+ else
+ warn "Presets: ${YELLOW}none found${RST} in $preset_dir"
+ fi
+ fi
+
+ echo ""
+ set -e
+}
+
+# --- Completions output -----------------------------------------------------
+print_completions() {
+ cat <<'COMP'
+_qemu_cli_completions() {
+ local cmds="install test mesh swarm snapshot chaos fuzz nvs health status help"
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ if [[ $COMP_CWORD -eq 1 ]]; then
+ COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
+ fi
+}
+complete -F _qemu_cli_completions qemu-cli.sh
+COMP
+}
+
+# --- Main dispatch ----------------------------------------------------------
+main() {
+ local cmd="${1:-help}"
+ shift 2>/dev/null || true
+
+ case "$cmd" in
+ install) cmd_install "$@" ;;
+ test) cmd_test "$@" ;;
+ mesh) cmd_mesh "$@" ;;
+ swarm) cmd_swarm "$@" ;;
+ snapshot) cmd_snapshot "$@" ;;
+ chaos) cmd_chaos "$@" ;;
+ fuzz) cmd_fuzz "$@" ;;
+ nvs) cmd_nvs "$@" ;;
+ health) cmd_health "$@" ;;
+ status) cmd_status "$@" ;;
+ help|-h|--help) cmd_help ;;
+ --version) echo "qemu-cli.sh v${VERSION}" ;;
+ --completions) print_completions ;;
+ *)
+ err "Unknown command: ${BOLD}${cmd}${RST}"
+ echo ""
+ cmd_help
+ exit 1
+ ;;
+ esac
+}
+
+main "$@"