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 "$@"