diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b5cf3bd..330d77d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,8 @@ "label": "Zacus: Firmware · Bootstrap venv", "type": "shell", "command": "bash", - "args": ["-lc", "cd hardware/firmware && ./tools/dev/bootstrap_local.sh"], + "args": ["-lc", "./tools/dev/bootstrap_local.sh"], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } }, @@ -15,8 +16,9 @@ "command": "bash", "args": [ "-lc", - "cd hardware/firmware && ./tools/dev/bootstrap_local.sh && export PLATFORMIO_CORE_DIR=\"$HOME/.platformio\" && ./build_all.sh" + "./tools/dev/bootstrap_local.sh && export PLATFORMIO_CORE_DIR=\"$HOME/.platformio\" && ./build_all.sh" ], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } }, @@ -25,7 +27,7 @@ "type": "shell", "command": "bash", "args": ["-lc", "./tools/dev/zacus.sh rc"], - "options": { "cwd": "${workspaceFolder}" }, + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } }, @@ -37,18 +39,7 @@ "-lc", "./tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md" ], - "options": { "cwd": "${workspaceFolder}" }, - "problemMatcher": [], - "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } - }, - { - "label": "Zacus: Firmware · RC live FAST (--skip-build --skip-upload)", - "type": "shell", - "command": "bash", - "args": [ - "-lc", - "cd hardware/firmware && ./tools/dev/bootstrap_local.sh && export PLATFORMIO_CORE_DIR=\"$HOME/.platformio\" && ZACUS_REQUIRE_HW=1 ./tools/dev/run_matrix_and_smoke.sh --skip-build --skip-upload" - ], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } }, @@ -58,8 +49,9 @@ "command": "bash", "args": [ "-lc", - "cd hardware/firmware && ./tools/dev/bootstrap_local.sh && while true; do echo \"==== $(date) ====\"; ./.venv/bin/python -m serial.tools.list_ports -v || true; echo; sleep 15; done" + "while true; do printf '==== %s ====\\n' \"$(date)\"; ./tools/dev/zacus.sh ports; sleep 15; done" ], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" } }, @@ -69,8 +61,9 @@ "command": "bash", "args": [ "-lc", - "cd hardware/firmware && while true; do d=$(ls -1dt artifacts/rc_live/* 2>/dev/null | head -n 1); if [ -n \"$d\" ]; then echo \"== $d ==\"; tail -n 60 \"$d/run_matrix_and_smoke.log\" 2>/dev/null || true; echo; fi; sleep 5; done" + "while true; do d=$(ls -1dt artifacts/rc_live/* 2>/dev/null | head -n 1); if [ -n \"$d\" ]; then echo \"== $d ==\"; tail -n 60 \"$d/run_matrix_and_smoke.log\" 2>/dev/null || true; echo; fi; sleep 5; done" ], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" } }, @@ -80,8 +73,9 @@ "command": "bash", "args": [ "-lc", - "cd hardware/firmware && d=$(ls -1dt artifacts/rc_live/* 2>/dev/null | head -n 1); echo \"Last: ${d:-n/a}\"; [ -n \"$d\" ] && sed -n '1,220p' \"$d/summary.md\" || true" + "d=$(ls -1dt artifacts/rc_live/* 2>/dev/null | head -n 1); echo \"Last: ${d:-n/a}\"; [ -n \"$d\" ] && sed -n '1,220p' \"$d/summary.md\" || true" ], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } }, @@ -90,8 +84,12 @@ "type": "shell", "command": "bash", "args": ["-lc", "echo \"Cockpit started (stop tasks to close watchers).\""], + "options": { "cwd": "${workspaceFolder}/hardware/firmware" }, "dependsOrder": "parallel", - "dependsOn": ["Zacus: Firmware · Ports watch (15s)", "Zacus: Firmware · Tail last RC log (5s)"], + "dependsOn": [ + "Zacus: Firmware · Ports watch (15s)", + "Zacus: Firmware · Tail last RC log (5s)" + ], "problemMatcher": [], "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } } diff --git a/README.md b/README.md index 950d5d1..34e48fc 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Dossier : `printables/` - `docs/index.md` + `docs/repo-status.md` pour la navigation et l’état du dépôt - `include-humain-IA/` (contenus annexes, renommé pour la portabilité) - `tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md` lance le prompt de Codex directement depuis le dépôt. +- Les artifacts de résolution de ports sont écrits sous `artifacts/ports//ports_resolve.json` avec un pointeur `artifacts/ports/latest_ports_resolve.json`. Utilisez `ZACUS_MOCK_PORTS=1` (et `ZACUS_PORTS_FIXTURE=tools/test/fixtures/ports_list_macos.txt`) pour tester la boucle sans matériel. +- `tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md` lance le prompt de Codex directement depuis le dépôt. ## État du projet - **Kit MJ** : complet (script, plan, antic-chaos, solution, checklists, export PDF). diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index df08510..fe00263 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -51,6 +51,10 @@ python3 tools/test/run_serial_suite.py --suite smoke_plus --allow-no-hardware python3 tools/test/zacus_menu.py --action smoke --allow-no-hardware ``` +Pour résoudre les ports sans matériel, la commande `./tools/dev/zacus.sh ports` écrit le JSON contractuel dans `artifacts/ports//ports_resolve.json` et met à jour `artifacts/ports/latest_ports_resolve.json`. Pour mocker les CP2102 utilisez `ZACUS_MOCK_PORTS=1 ZACUS_PORTS_FIXTURE=tools/test/fixtures/ports_list_macos.txt ./tools/dev/zacus.sh ports`. + +Codex CLI intégré : `./tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md`. Le script note aussi l’état des ports résolus et place les logs dans `artifacts/codex//`. + Dépendances optionnelles: - `pip install pyyaml` pour `run_content_checks.sh` - `pip install pyserial` pour suites USB, UI Link sim et console série diff --git a/hardware/firmware/.vscode/tasks.json b/hardware/firmware/.vscode/tasks.json index 5589614..91a0416 100644 --- a/hardware/firmware/.vscode/tasks.json +++ b/hardware/firmware/.vscode/tasks.json @@ -5,10 +5,7 @@ "label": "Zacus: RC Live", "type": "shell", "command": "bash", - "args": [ - "-lc", - "set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" rc" - ], + "args": ["-lc", "set -euo pipefail; ./tools/dev/zacus.sh rc"], "options": { "cwd": "${workspaceFolder}" }, "presentation": { "panel": "dedicated", "reveal": "always" }, "problemMatcher": [] @@ -19,7 +16,7 @@ "command": "bash", "args": [ "-lc", - "set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" flash && \"$ROOT/tools/dev/zacus.sh\" rc" + "set -euo pipefail; ./tools/dev/zacus.sh flash && ./tools/dev/zacus.sh rc" ], "options": { "cwd": "${workspaceFolder}" }, "presentation": { "panel": "dedicated", "reveal": "always" }, @@ -29,10 +26,7 @@ "label": "Zacus: RC Live + AutoFix", "type": "shell", "command": "bash", - "args": [ - "-lc", - "set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" rc-autofix" - ], + "args": ["-lc", "set -euo pipefail; ./tools/dev/zacus.sh rc-autofix"], "options": { "cwd": "${workspaceFolder}" }, "presentation": { "panel": "dedicated", "reveal": "always" }, "problemMatcher": [] @@ -43,10 +37,14 @@ "command": "bash", "args": [ "-lc", - "set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" ports" + "set -euo pipefail; while true; do ./tools/dev/zacus.sh ports; sleep 15; done" ], "options": { "cwd": "${workspaceFolder}" }, - "presentation": { "panel": "dedicated", "reveal": "always", "showReuseMessage": false }, + "presentation": { + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": false + }, "problemMatcher": [] } ] diff --git a/hardware/firmware/tools/dev/codex_prompt_menu.sh b/hardware/firmware/tools/dev/codex_prompt_menu.sh index 71e98ed..d7be991 100755 --- a/hardware/firmware/tools/dev/codex_prompt_menu.sh +++ b/hardware/firmware/tools/dev/codex_prompt_menu.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail + +# --- Codex Prompt Menu TUI --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel)" FW_ROOT="$REPO_ROOT/hardware/firmware" @@ -8,6 +10,15 @@ PROMPT_DIR="$FW_ROOT/tools/dev/codex_prompts" ARTIFACTS_DIR="$FW_ROOT/artifacts/rc_live" LAST_MESSAGE_FILE="$ARTIFACTS_DIR/_codex_last_message.md" +# Détection TUI (dialog/whiptail) +if command -v dialog >/dev/null 2>&1; then + TUI_CMD="dialog" +elif command -v whiptail >/dev/null 2>&1; then + TUI_CMD="whiptail" +else + TUI_CMD="" +fi + collect_prompts() { local glob prompt_files=() @@ -17,6 +28,8 @@ collect_prompts() { done } + +# Menu interactif pour choisir un prompt (TUI si possible) main_menu() { collect_prompts if [[ ${#prompt_files[@]} -eq 0 ]]; then @@ -24,25 +37,43 @@ main_menu() { exit 1 fi while true; do - echo - echo "Available prompts:" - for idx in "${!prompt_files[@]}"; do - printf "%2d) %s\n" $((idx + 1)) "$(basename "${prompt_files[idx]}")" - done - read -rp "Choice (q to quit): " choice - if [[ "$choice" =~ ^[Qq]$ ]]; then - exit 0 + local choice + if [[ -n "$TUI_CMD" ]]; then + local menu_args=() + for idx in "${!prompt_files[@]}"; do + menu_args+=("$idx" "$(basename "${prompt_files[idx]}")") + done + choice=$( \ + $TUI_CMD --clear --title "Codex Prompt Menu" \ + --menu "Sélectionnez un prompt :" 20 70 12 \ + "${menu_args[@]}" \ + 3>&1 1>&2 2>&3 + ) + if [[ -z "$choice" ]]; then + exit 0 + fi + run_prompt "${prompt_files[choice]}" + else + echo + echo "Available prompts:" + for idx in "${!prompt_files[@]}"; do + printf "%2d) %s\n" $((idx + 1)) "$(basename "${prompt_files[idx]}")" + done + read -rp "Choice (q to quit): " choice + if [[ "$choice" =~ ^[Qq]$ ]]; then + exit 0 + fi + if ! [[ "$choice" =~ ^[0-9]+$ ]]; then + echo "Please enter a number or 'q'." + continue + fi + choice=$((choice - 1)) + if (( choice < 0 || choice >= ${#prompt_files[@]} )); then + echo "Selection out of range." + continue + fi + run_prompt "${prompt_files[choice]}" fi - if ! [[ "$choice" =~ ^[0-9]+$ ]]; then - echo "Please enter a number or 'q'." - continue - fi - choice=$((choice - 1)) - if (( choice < 0 || choice >= ${#prompt_files[@]} )); then - echo "Selection out of range." - continue - fi - run_prompt "${prompt_files[choice]}" done } diff --git a/hardware/firmware/tools/dev/zacus.sh b/hardware/firmware/tools/dev/zacus.sh index 174f27f..c3aad87 100755 --- a/hardware/firmware/tools/dev/zacus.sh +++ b/hardware/firmware/tools/dev/zacus.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail + +# --- Zacus.sh TUI --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" ARTIFACT_ROOT="$REPO_ROOT/artifacts/rc_live" @@ -8,6 +10,15 @@ RESOLVER="$REPO_ROOT/tools/test/resolve_ports.py" RC_RUNNER="$REPO_ROOT/tools/dev/run_matrix_and_smoke.sh" PROMPT_DIR="$SCRIPT_DIR/codex_prompts" +# Détection TUI (dialog/whiptail) +if command -v dialog >/dev/null 2>&1; then + TUI_CMD="dialog" +elif command -v whiptail >/dev/null 2>&1; then + TUI_CMD="whiptail" +else + TUI_CMD="" +fi + require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "[zacus] missing command: $1" >&2 @@ -193,10 +204,36 @@ HELP mkdir -p "$ARTIFACT_ROOT" + +# Menu interactif si aucun argument fourni command=${1:-} if [[ -z "$command" ]]; then - usage - exit 1 + if [[ -n "$TUI_CMD" ]]; then + choice=$( \ + $TUI_CMD --clear --title "Zacus CLI" \ + --menu "Sélectionnez une commande :" 20 70 10 \ + bootstrap "bootstrap tooling" \ + build "run build_all.sh" \ + flash "upload esp32 + esp8266 via resolved ports" \ + rc "strict RC live (ZACUS_REQUIRE_HW=1)" \ + rc-autofix "RC + codex autofix loop" \ + ports "ports watch (15s)" \ + latest "show latest RC artifact path" \ + exit "exit" \ + 3>&1 1>&2 2>&3 + ) + if [[ -z "$choice" || "$choice" == "exit" ]]; then + exit 0 + fi + command="$choice" + else + echo "Usage: zacus.sh " + echo "Commandes disponibles : bootstrap, build, flash, rc, rc-autofix, ports, latest" + read -rp "Commande : " command + if [[ -z "$command" || "$command" == "exit" ]]; then + exit 0 + fi + fi fi case "$command" in diff --git a/hardware/firmware/tools/dev/zacus_menu.sh b/hardware/firmware/tools/dev/zacus_menu.sh index 0909e34..30376df 100755 --- a/hardware/firmware/tools/dev/zacus_menu.sh +++ b/hardware/firmware/tools/dev/zacus_menu.sh @@ -1,6 +1,15 @@ #!/usr/bin/env bash + set -euo pipefail +# DEBUG: Affichage de début de script +echo "[DEBUG] Lancement du cockpit Zacus..." >&2 +sleep 0.1 +export LC_ALL=C +export LANG=C + + +# --- Zacus Menu TUI --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(git -C "$SCRIPT_DIR/../.." rev-parse --show-toplevel)" FW_ROOT="$REPO_ROOT/hardware/firmware" @@ -8,17 +17,36 @@ ARTIFACTS_ROOT="$FW_ROOT/artifacts/rc_live" PROMPT_DIR="$FW_ROOT/tools/dev/codex_prompts" RC_PROMPT="$PROMPT_DIR/rc_live_fail.prompt.md" +# Détection TUI (fzf > dialog > whiptail) +if command -v fzf >/dev/null 2>&1; then + TUI_CMD="fzf" +elif command -v dialog >/dev/null 2>&1; then + TUI_CMD="dialog" +elif command -v whiptail >/dev/null 2>&1; then + TUI_CMD="whiptail" +else + TUI_CMD="" +fi + +# Message d'invitation à installer fzf si aucun TUI +if [[ -z "$TUI_CMD" ]]; then + echo -e "\033[1;33m[Astuce] Pour une navigation moderne (flèches + Entrée/Échap), installez fzf :\033[0m" >&2 + echo -e "\033[1;36mbrew install fzf\033[0m (ou sudo apt install fzf sous Linux)" >&2 +fi + + + run_bootstrap() { - (cd "$REPO_ROOT" && ./tools/dev/bootstrap_local.sh) + "$FW_ROOT/tools/dev/bootstrap_local.sh" } run_build_all() { - (cd "$FW_ROOT" && ./build_all.sh) + "$FW_ROOT/build_all.sh" } run_rc_live() { local artifacts="" - if ! (cd "$FW_ROOT" && ./tools/dev/run_matrix_and_smoke.sh); then + if ! "$FW_ROOT/tools/dev/run_matrix_and_smoke.sh"; then artifacts="$(latest_artifacts)" if [[ -n "$artifacts" && -f "$RC_PROMPT" ]]; then ARTIFACT_PATH="$artifacts" codex exec - < "$RC_PROMPT" @@ -46,34 +74,212 @@ latest_artifacts() { ls -1d "$ARTIFACTS_ROOT"/*/ 2>/dev/null | sort | tail -n1 } -menu() { - cat <<'EOF' -Firmware cockpit -1) bootstrap (tools/dev/bootstrap_local.sh) -2) build all firmware (hardware/firmware/build_all.sh) -3) rc live gate (ZACUS_REQUIRE_HW unset) -4) rc live gate (ZACUS_REQUIRE_HW=1) -5) watch serial ports -6) run codex prompt menu -0) exit -EOF + +# Affichage du menu (fzf > dialog > texte coloré) +show_menu() { + local options=( + "1:RC live gate (ZACUS_REQUIRE_HW unset)" + "2:RC live gate (ZACUS_REQUIRE_HW=1)" + "3:Build all firmware (hardware/firmware/build_all.sh)" + "4:Bootstrap (outils/dev/bootstrap_local.sh)" + "5:Watch serial ports" + "6:Run codex prompt menu" + "7:Afficher logs (firmware/logs/)" + "0:Exit" + ) + if [[ "$TUI_CMD" == "fzf" ]]; then + local choice + choice=$(printf '%s\n' "${options[@]}" | fzf --ansi --prompt="❯ Zacus Firmware Cockpit : " --header="[Flèches] naviguer, [Entrée] valider, [Échap] quitter" --height=15 --border --cycle) + [[ -z "$choice" ]] && echo 0 && return + echo "${choice%%:*}" + elif [[ "$TUI_CMD" == "dialog" || "$TUI_CMD" == "whiptail" ]]; then + local menu_args=() + for opt in "${options[@]}"; do + menu_args+=("${opt%%:*}" "${opt#*:}") + done + local choice + if [[ "$TUI_CMD" == "dialog" ]]; then + choice=$(dialog --clear --colors --title "\Zb\Z4Zacus Firmware Cockpit\Zn" \ + --menu "\Z1Sélectionnez une action :\Zn" 20 70 10 \ + "${menu_args[@]}" \ + 3>&1 1>&2 2>&3) + else + choice=$(whiptail --title "Zacus Firmware Cockpit" \ + --menu "Sélectionnez une action :" 20 70 10 \ + "${menu_args[@]}" \ + 3>&1 1>&2 2>&3) + fi + [[ -z "$choice" ]] && echo 0 && return + echo "$choice" + else + # Fallback texte coloré, toujours affiché tant qu'un choix valide n'est pas fait + local Y="\033[1;33m" G="\033[1;32m" C="\033[1;36m" R="\033[1;31m" N="\033[0m" + echo -e "${R}[Aucun menu TUI détecté : utilisation du mode texte interactif]${N}" >&2; >&2 + echo "[DEBUG] TEST affichage menu texte" >&2; >&2 + trap 'echo; exit 0' INT + while true; do + echo -e "${C}╔══════════════════════════════════════════════════════╗${N}" >&2; >&2 + echo -e "${C}║ ${G}Zacus Firmware Cockpit (mode texte)${C} ║${N}" >&2; >&2 + echo -e "${C}╚══════════════════════════════════════════════════════╝${N}" >&2; >&2 + echo -e "${Y}Navigation : Entrez le numéro puis [Entrée]. Annulez avec Ctrl+C.${N}" >&2; >&2 + echo -e "${Y}Pour une navigation à la souris ou aux flèches, installez fzf !${N}" >&2; >&2 + for opt in "${options[@]}"; do + printf " ${Y}%s${N} %s\n" "${opt%%:*}" "${opt#*:}" >&2; >&2 + done + echo -en "${G}Votre choix [0-7] : ${N}" >&2; >&2 + read -r choice || { echo; exit 0; } + [[ -z "$choice" || "$choice" == "0" ]] && echo 0 && return + if [[ "$choice" =~ ^[1-7]$ ]]; then + echo "$choice" + return + fi + echo -e "${R}Entrée invalide. Veuillez choisir un numéro entre 0 et 7.${N}" >&2; >&2 + done + # Sous-menu pour afficher les logs + afficher_logs_menu() { + local logs_dir="$FW_ROOT/logs" + local files=() + local file + # Liste les fichiers de log (récents d'abord) + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find "$logs_dir" -type f -print0 | sort -z -r) + if [[ ${#files[@]} -eq 0 ]]; then + echo "Aucun fichier de log trouvé dans $logs_dir." >&2 + sleep 2 + return + fi + local choix + if [[ "$TUI_CMD" == "fzf" ]]; then + choix=$(printf '%s\n' "${files[@]}" | fzf --ansi --prompt="❯ Sélectionnez un log : " --header="[Flèches] naviguer, [Entrée] afficher, [Échap] retour" --height=15 --border) + [[ -z "$choix" ]] && return + elif [[ "$TUI_CMD" == "dialog" || "$TUI_CMD" == "whiptail" ]]; then + local menu_args=() + local idx=1 + for f in "${files[@]}"; do + menu_args+=("$idx" "$(basename "$f")") + ((idx++)) + done + if [[ "$TUI_CMD" == "dialog" ]]; then + idx=$(dialog --clear --title "Logs firmware" --menu "Sélectionnez un log :" 20 70 15 "${menu_args[@]}" 3>&1 1>&2 2>&3) + else + idx=$(whiptail --title "Logs firmware" --menu "Sélectionnez un log :" 20 70 15 "${menu_args[@]}" 3>&1 1>&2 2>&3) + fi + [[ -z "$idx" ]] && return + choix="${files[idx-1]}" + else + echo -e "\033[1;36mFichiers de log disponibles :\033[0m" >&2 + local i=1 + for f in "${files[@]}"; do + echo " $i) $(basename "$f")" >&2 + ((i++)) + done + echo -en "Numéro du log à afficher (ou Entrée pour retour) : " >&2 + read -r idx + [[ -z "$idx" ]] && return + if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx >= 1 && idx <= ${#files[@]} )); then + choix="${files[idx-1]}" + else + echo "Entrée invalide." >&2 + sleep 1 + return + fi + fi + echo -e "\033[1;33m--- Dernières lignes de : $choix ---\033[0m" + tail -n 15 "$choix" + echo -e "\033[1;36m--- Appuyez sur Entrée pour revenir au menu ---\033[0m" + read -r _ + } + fi } run_codex_prompts() { "$FW_ROOT/tools/dev/codex_prompt_menu.sh" } + + +# Sous-menu pour afficher les logs (doit être défini avant la boucle principale) +afficher_logs_menu() { + local logs_dir="$FW_ROOT/logs" + local files=() + local file + # Liste les fichiers de log (récents d'abord) + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find "$logs_dir" -type f -print0 | sort -z -r) + if [[ ${#files[@]} -eq 0 ]]; then + echo "Aucun fichier de log trouvé dans $logs_dir." >&2 + sleep 2 + return + fi + local choix + if [[ "$TUI_CMD" == "fzf" ]]; then + choix=$(printf '%s\n' "${files[@]}" | fzf --ansi --prompt="❯ Sélectionnez un log : " --header="[Flèches] naviguer, [Entrée] afficher, [Échap] retour" --height=15 --border) + [[ -z "$choix" ]] && return + elif [[ "$TUI_CMD" == "dialog" || "$TUI_CMD" == "whiptail" ]]; then + local menu_args=() + local idx=1 + for f in "${files[@]}"; do + menu_args+=("$idx" "$(basename "$f")") + ((idx++)) + done + if [[ "$TUI_CMD" == "dialog" ]]; then + idx=$(dialog --clear --title "Logs firmware" --menu "Sélectionnez un log :" 20 70 15 "${menu_args[@]}" 3>&1 1>&2 2>&3) + else + idx=$(whiptail --title "Logs firmware" --menu "Sélectionnez un log :" 20 70 15 "${menu_args[@]}" 3>&1 1>&2 2>&3) + fi + [[ -z "$idx" ]] && return + choix="${files[idx-1]}" + else + echo -e "\033[1;36mFichiers de log disponibles :\033[0m" >&2 + local i=1 + for f in "${files[@]}"; do + echo " $i) $(basename "$f")" >&2 + ((i++)) + done + echo -en "Numéro du log à afficher (ou Entrée pour retour) : " >&2 + read -r idx + [[ -z "$idx" ]] && return + if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx >= 1 && idx <= ${#files[@]} )); then + choix="${files[idx-1]}" + else + echo "Entrée invalide." >&2 + sleep 1 + return + fi + fi + echo -e "\033[1;33m--- Dernières lignes de : $choix ---\033[0m" + tail -n 15 "$choix" + echo -e "\033[1;36m--- Appuyez sur Entrée pour revenir au menu ---\033[0m" + read -r _ +} + +# Boucle principale avec menu TUI amélioré + +# Forçage du menu texte si aucun TUI détecté +if [[ -z "$TUI_CMD" ]]; then + echo "[INFO] Aucun menu TUI détecté, utilisation du menu texte interactif." >&2 +fi + while true; do - menu - read -rp "Choice: " choice + echo "[DEBUG] Affichage du menu..." >&2 + choice=$(show_menu) + echo "[DEBUG] Choix utilisateur: '$choice'" >&2 case "$choice" in - 1) run_bootstrap ;; - 2) run_build_all ;; - 3) ZACUS_REQUIRE_HW=0 run_rc_live ;; - 4) ZACUS_REQUIRE_HW=1 run_rc_live ;; + 1) ZACUS_REQUIRE_HW=0 run_rc_live ;; + 2) ZACUS_REQUIRE_HW=1 run_rc_live ;; + 3) run_build_all ;; + 4) run_bootstrap ;; 5) ports_watch ;; 6) run_codex_prompts ;; - 0) exit 0 ;; - *) echo "Unknown option: $choice" ;; + 7) afficher_logs_menu ;; + 0|""|"Cancel") + echo "[DEBUG] Sortie du cockpit Zacus." >&2 + [[ "$TUI_CMD" == "dialog" ]] && dialog --msgbox "Sortie du cockpit Zacus." 6 40 + exit 0 ;; + *) + echo "[DEBUG] Entrée non reconnue, relance du menu." >&2 + ;; esac done diff --git a/tools/dev/zacus.sh b/tools/dev/zacus.sh index ce792eb..6fa7fb8 100755 --- a/tools/dev/zacus.sh +++ b/tools/dev/zacus.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" FW_ROOT="$REPO_ROOT/hardware/firmware" RC_RUNNER="$FW_ROOT/tools/dev/run_matrix_and_smoke.sh" SMOKE_SCRIPT="$FW_ROOT/tools/dev/serial_smoke.py" @@ -11,6 +11,7 @@ ARTIFACT_ROOT="$REPO_ROOT/artifacts" PORTS_ARTIFACT_ROOT="$ARTIFACT_ROOT/ports" LATEST_PORTS_JSON="$PORTS_ARTIFACT_ROOT/latest_ports_resolve.json" CODEX_ARTIFACT_ROOT="$ARTIFACT_ROOT/codex" +DEFAULT_PORTS_FIXTURE="$REPO_ROOT/tools/test/fixtures/ports_list_macos.txt" LAST_RESOLVED_JSON="" info() { @@ -22,15 +23,20 @@ die() { exit 1 } +die_codex() { + printf '[zacus] ERROR: %s\n' "$*" >&2 + exit 2 +} + usage() { cat <<'USAGE' Usage: zacus.sh [args] Commands: - rc Run the RC live gate (requires hardware) + rc Run the RC live gate (always strict) smoke Run the serial smoke helper (pass --role ... args) ports Resolve ports once and show summary - codex Invoke codex exec with a prompt (see --help) - menu Show the zacus cockpit menu + codex Invoke Codex CLI with a prompt + menu Show the Zacus cockpit menu help Show this usage USAGE } @@ -41,24 +47,38 @@ print_ports_summary() { printf '[ports] no resolved data yet (run "zacus.sh ports" to resolve)\n' return 1 fi - python3 - <<'PY' + python3 - "$json_file" <<'PY' +import datetime import json import pathlib import sys path = pathlib.Path(sys.argv[1]) -data = json.loads(path.read_text(encoding='utf-8')) -ports = data.get('ports', {}) -reasons = data.get('reasons', {}) -notes = data.get('notes', []) -labels = [('esp32', 'esp32_audio'), ('esp8266', 'esp8266_oled')] -print(f'[ports] summary ({path.name}):') -for role, label in labels: - port = ports.get(role) or 'n/a' - reason = reasons.get(role) or 'unset' - print(f' - {label}: {port} (reason={reason})') -print(' - rp2040_tft: not auto-resolved (manual selection)') -print('[ports] notes:', ' | '.join(notes) if notes else 'none') +try: + data = json.loads(path.read_text(encoding='utf-8')) + ports = data.get('ports', {}) + reasons = data.get('reasons', {}) + notes = data.get('notes', []) + labels = [('esp32', 'esp32_audio'), ('esp8266', 'esp8266_oled')] + timestamp = path.parent.name + age = 'unknown' + try: + dt = datetime.datetime.strptime(timestamp, '%Y%m%d-%H%M%S') + dt = dt.replace(tzinfo=datetime.timezone.utc) + age_delta = datetime.datetime.now(datetime.timezone.utc) - dt + age = str(age_delta).split('.')[0] + except Exception: + pass + print(f'[ports] summary (json={path.name}) timestamp={timestamp} age={age}') + for role, label in labels: + port = ports.get(role) or 'n/a' + reason = reasons.get(role) or 'unset' + print(f' - {label}: {port} (reason={reason})') + print(' - rp2040_tft: not auto-resolved (manual selection)') + print(f'[ports] json path: {path}') + print('[ports] notes:', ' | '.join(notes) if notes else 'none') +except Exception as exc: + print(f'[ports] summary unavailable: {exc}') PY } @@ -69,24 +89,64 @@ resolve_ports_once() { local dir="$PORTS_ARTIFACT_ROOT/$timestamp" mkdir -p "$dir" local json="$dir/ports_resolve.json" + local resolver_args=(python3 "$REPO_ROOT/tools/test/resolve_ports.py" \ + --need-esp32 --need-esp8266 --ports-resolve-json "$json" --wait-port 3) + local use_fixture=0 + local fixture_path="" + if [[ "${ZACUS_MOCK_PORTS:-0}" == "1" ]]; then + use_fixture=1 + fixture_path="${ZACUS_PORTS_FIXTURE:-$DEFAULT_PORTS_FIXTURE}" + elif [[ -n "${ZACUS_PORTS_FIXTURE:-}" ]]; then + use_fixture=1 + fixture_path="${ZACUS_PORTS_FIXTURE}" + fi + if (( use_fixture == 1 )); then + resolver_args+=(--mock) + if [[ -n "$fixture_path" && -f "$fixture_path" ]]; then + resolver_args+=(--ports-fixture "$fixture_path") + else + info "ports fixture not found: $fixture_path" + fi + fi info "resolving ports (output -> $json)" set +e - python3 "$REPO_ROOT/tools/test/resolve_ports.py" \ - --need-esp32 --need-esp8266 --ports-resolve-json "$json" --wait-port 3 + "${resolver_args[@]}" local rc=$? set -e if [[ -f "$json" ]]; then cp "$json" "$LATEST_PORTS_JSON" 2>/dev/null || true + LAST_RESOLVED_JSON="$json" fi - LAST_RESOLVED_JSON="$json" return $rc } +show_ports_context() { + local candidate_json + if [[ -n "${LAST_RESOLVED_JSON}" && -f "$LAST_RESOLVED_JSON" ]]; then + candidate_json="$LAST_RESOLVED_JSON" + elif [[ -f "$LATEST_PORTS_JSON" ]]; then + candidate_json="$LATEST_PORTS_JSON" + else + printf '[ports] no resolution data yet\n' + return 1 + fi + print_ports_summary "$candidate_json" || true + if [[ -f "$LATEST_PORTS_JSON" ]]; then + printf '[ports] pointer: %s\n' "$LATEST_PORTS_JSON" + fi +} + cmd_ports() { resolve_ports_once || true - if [[ -n "${LAST_RESOLVED_JSON:-}" ]]; then - print_ports_summary "$LAST_RESOLVED_JSON" || true - printf '[ports] data file: %s\n' "$LAST_RESOLVED_JSON" + local json_file="" + if [[ -n "${LAST_RESOLVED_JSON}" && -f "$LAST_RESOLVED_JSON" ]]; then + json_file="$LAST_RESOLVED_JSON" + elif [[ -f "$LATEST_PORTS_JSON" ]]; then + json_file="$LATEST_PORTS_JSON" + fi + if [[ -n "$json_file" ]]; then + print_ports_summary "$json_file" || true + printf '[ports] data file: %s\n' "$json_file" else printf '[ports] resolver did not produce a JSON file\n' fi @@ -107,10 +167,10 @@ cmd_smoke() { ensure_codex_ready() { if ! command -v codex >/dev/null 2>&1; then - die "codex CLI not found. Install it from https://app.codex.com/docs/cli" + die_codex "codex CLI not found. Install it from https://app.codex.com/docs/cli" fi if ! codex login status >/dev/null 2>&1; then - die "codex CLI is not logged in. Run 'codex login' before continuing." + die_codex "codex CLI is not logged in. Run 'codex login' before continuing." fi } @@ -118,7 +178,6 @@ cmd_codex() { local prompt_file="" local sandbox="workspace-write" local auto_mode="0" - local custom_sandbox="" while [[ $# -gt 0 ]]; do case "$1" in --prompt) @@ -154,16 +213,17 @@ HELP fi ensure_codex_ready resolve_ports_once || true - if [[ -n "${LAST_RESOLVED_JSON:-}" ]]; then - print_ports_summary "$LAST_RESOLVED_JSON" || true + local ports_json="${LAST_RESOLVED_JSON:-$LATEST_PORTS_JSON}" + if [[ -n "$ports_json" && -f "$ports_json" ]]; then + print_ports_summary "$ports_json" || true fi mkdir -p "$CODEX_ARTIFACT_ROOT" local ts ts=$(date -u +%Y%m%d-%H%M%S) local art_dir="$CODEX_ARTIFACT_ROOT/$ts" mkdir -p "$art_dir" - if [[ -f "$LAST_RESOLVED_JSON" ]]; then - cp "$LAST_RESOLVED_JSON" "$art_dir/ports_resolve.json" 2>/dev/null || true + if [[ -n "$ports_json" && -f "$ports_json" ]]; then + cp "$ports_json" "$art_dir/ports_resolve.json" 2>/dev/null || true fi printf 'prompt=%s\nsandbox=%s\nauto=%s\n' "$prompt_file" "$sandbox" "$auto_mode" > "$art_dir/metadata.txt" local exec_cmd=(codex exec --sandbox "$sandbox") @@ -241,9 +301,29 @@ menu_run_codex() { } cmd_menu() { + local non_tui=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --no-tui) + non_tui=1 + ;; + --help|-h) + usage + return 0 + ;; + *) + die "unknown menu option: $1" + ;; + esac + shift + done + if (( non_tui == 1 )); then + cmd_ports + return 0 + fi while true; do printf '\n=== Zacus cockpit ===\n' - print_ports_summary "${LATEST_PORTS_JSON}" || true + show_ports_context || true printf '[menu] options:\n' printf ' 1) RC live\n' printf ' 2) Serial smoke\n' @@ -252,12 +332,24 @@ cmd_menu() { printf ' 0) Exit\n' read -rp 'Choice: ' choice case "$choice" in - 1) cmd_rc ;; - 2) cmd_smoke ;; - 3) menu_run_codex ;; - 4) cmd_ports ;; - 0) return 0 ;; - *) printf 'unknown choice\n' ;; + 1) + cmd_rc + ;; + 2) + cmd_smoke + ;; + 3) + menu_run_codex + ;; + 4) + cmd_ports + ;; + 0) + return 0 + ;; + *) + printf 'unknown choice\n' + ;; esac done } @@ -278,7 +370,7 @@ case "$command" in cmd_codex "$@" ;; menu) - cmd_menu + cmd_menu "$@" ;; help|-h|--help) usage diff --git a/tools/test/fixtures/ports_list_macos.txt b/tools/test/fixtures/ports_list_macos.txt new file mode 100644 index 0000000..05d7f78 --- /dev/null +++ b/tools/test/fixtures/ports_list_macos.txt @@ -0,0 +1,2 @@ +/dev/cu.SLAB_USBtoUART1,20-6.1.1,ESP32 dev kit,10C4:EA60 +/dev/cu.SLAB_USBtoUART2,20-6.1.2,ESP8266 OLED,10C4:EA60 diff --git a/tools/test/resolve_ports.py b/tools/test/resolve_ports.py index 26ea17a..cc3fbe3 100755 --- a/tools/test/resolve_ports.py +++ b/tools/test/resolve_ports.py @@ -11,14 +11,17 @@ import re import sys import time from pathlib import Path +import csv from typing import Dict, List, Optional, Tuple +PY_SERIAL_ERROR = "" try: import serial from serial.tools import list_ports -except ImportError: - print(json.dumps({"status": "fail", "notes": ["pyserial missing: pip install pyserial"]})) - raise SystemExit(2) +except ImportError as exc: + serial = None + list_ports = None + PY_SERIAL_ERROR = str(exc) REPO_ROOT = Path(__file__).resolve().parents[2] @@ -37,6 +40,40 @@ ESP8266_SIGNATURE = re.compile(r"(ets Jan|Exception \(|Stack smashing|\[SCREEN\] FINGERPRINT_BAUDS = (115200, 19200) FINGERPRINT_TIMEOUT = 2.0 +DEFAULT_PORTS_FIXTURE = REPO_ROOT / "tools" / "test" / "fixtures" / "ports_list_macos.txt" + + +class MockPort: + def __init__( + self, + device: str, + location: str = "", + description: str = "", + manufacturer: str = "", + product: str = "", + vid: int = 0, + pid: int = 0, + ) -> None: + self.device = device + self.location = location + self.description = description + self.manufacturer = manufacturer + self.product = product + self.vid = vid + self.pid = pid + + @property + def hwid(self) -> str: + parts = [] + if self.vid is not None and self.pid is not None: + parts.append(f"USB VID:PID={self.vid:04X}:{self.pid:04X}") + if self.location: + parts.append(f"LOCATION={self.location}") + return " ".join(parts) + + +FIXTURE_PORTS: List["MockPort"] = [] + def is_bluetooth_port(device: str) -> bool: lower = device.lower() @@ -65,6 +102,43 @@ def parse_location(port) -> str: return "" +def load_ports_fixture(fixture_path: Path) -> List[MockPort]: + if not fixture_path.exists(): + return [] + ports: List[MockPort] = [] + with fixture_path.open(encoding="utf-8") as handle: + reader = csv.reader(handle) + for row in reader: + if not row or row[0].strip().startswith("#"): + continue + device = row[0].strip() + if not device: + continue + location = row[1].strip() if len(row) > 1 else "" + description = row[2].strip() if len(row) > 2 else "mock" + vidpid = row[3].strip() if len(row) > 3 else "" + vid = pid = 0 + if vidpid and ":" in vidpid: + parts = vidpid.split(":", 1) + try: + vid = int(parts[0], 16) + pid = int(parts[1], 16) + except Exception: + vid = pid = 0 + ports.append( + MockPort( + device=device, + location=location, + description=description, + manufacturer="mock", + product="mock", + vid=vid, + pid=pid, + ) + ) + return ports + + def load_map_file(path: Path) -> Dict[str, Dict[str, str]]: data = {"location": {}, "vidpid": {}} if not path.exists(): @@ -266,6 +340,10 @@ def choose_interactive(candidates: List[dict], role: str) -> Optional[str]: def gather_ports(wait_port: int) -> List: deadline = time.monotonic() + max(1, wait_port) + if FIXTURE_PORTS: + return FIXTURE_PORTS + if list_ports is None: + return [] while True: ports = list(list_ports.comports()) if ports: @@ -411,6 +489,8 @@ def main() -> int: parser.add_argument("--auto-ports", dest="auto_ports", action="store_true", default=True) parser.add_argument("--no-auto-ports", dest="auto_ports", action="store_false") parser.add_argument("--prefer-cu", action="store_true") + parser.add_argument("--mock", action="store_true") + parser.add_argument("--ports-fixture", default="") parser.add_argument("--interactive", action="store_true") parser.add_argument("--ports-resolve-json", default="") args = parser.parse_args() @@ -430,6 +510,20 @@ def main() -> int: args.port_esp8266 = env_esp8266 env_override["esp8266"] = True + env_mock = os.environ.get("ZACUS_MOCK_PORTS", "0") == "1" + fixture_arg = args.ports_fixture or os.environ.get("ZACUS_PORTS_FIXTURE", "") + use_mock = args.mock or env_mock or bool(fixture_arg) + mock_notes: List[str] = [] + if use_mock: + fixture_path = Path(fixture_arg) if fixture_arg else DEFAULT_PORTS_FIXTURE + if fixture_path.exists(): + FIXTURE_PORTS.clear() + FIXTURE_PORTS.extend(load_ports_fixture(fixture_path)) + if FIXTURE_PORTS: + mock_notes.append(f"mock fixture={fixture_path.name}") + else: + mock_notes.append(f"mock fixture missing: {fixture_path}") + ports_map = load_ports_map() initial_ports = {"esp32": args.port_esp32, "esp8266": args.port_esp8266} @@ -458,6 +552,9 @@ def main() -> int: }, "notes": [], } + result["notes"].extend(mock_notes) + if PY_SERIAL_ERROR and not use_mock: + result["notes"].append(f"pyserial missing: {PY_SERIAL_ERROR}") missing_roles = [] if args.need_esp32 and not result["ports"]["esp32"]: diff --git a/tools/test/test_zacus_cli.sh b/tools/test/test_zacus_cli.sh new file mode 100755 index 0000000..b6afb70 --- /dev/null +++ b/tools/test/test_zacus_cli.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +FIXTURE="$ROOT/tools/test/fixtures/ports_list_macos.txt" +ZACUS_CMD="$ROOT/tools/dev/zacus.sh" + +run_help() { + printf '== running help ==\n' + ZACUS_MOCK_PORTS=1 ZACUS_PORTS_FIXTURE="$FIXTURE" "$ZACUS_CMD" help >/dev/null +} + +run_ports() { + printf '== running ports (mock) ==\n' + ZACUS_MOCK_PORTS=1 ZACUS_PORTS_FIXTURE="$FIXTURE" "$ZACUS_CMD" ports >/dev/null + local json="$ROOT/artifacts/ports/latest_ports_resolve.json" + if [[ ! -f "$json" ]]; then + printf 'ports json missing: %s\n' "$json" + exit 1 + fi + python3 - "$json" <<'PY' +import json, pathlib, sys +path = pathlib.Path(sys.argv[1]) +data = json.loads(path.read_text(encoding='utf-8')) +if 'status' not in data: + raise SystemExit('status missing') +print('ports status:', data['status']) +PY +} + +run_codex_expect_fail() { + printf '== running codex (expect exit 2) ==\n' + set +e + PATH="/bin:/usr/bin" ZACUS_MOCK_PORTS=1 ZACUS_PORTS_FIXTURE="$FIXTURE" "$ZACUS_CMD" codex --prompt "$ROOT/tools/dev/codex_prompts/zacus_hw_now.md" + local code=$? + set -e + if [[ $code -ne 2 ]]; then + printf 'expected codex exit 2, got %d\n' "$code" + exit 1 + fi + printf 'codex exit as expected\n' +} + +run_menu_non_tui() { + printf '== running menu --no-tui ==\n' + ZACUS_MOCK_PORTS=1 ZACUS_PORTS_FIXTURE="$FIXTURE" "$ZACUS_CMD" menu --no-tui >/dev/null +} + +run_help +run_ports +run_codex_expect_fail +run_menu_non_tui