Harden Zacus CLI for mock workflows

This commit is contained in:
Clément SAILLANT
2026-02-15 17:02:55 +01:00
parent 194e0c101d
commit 1120c56dd3
11 changed files with 632 additions and 113 deletions
+17 -19
View File
@@ -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" }
}
+2
View File
@@ -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/<timestamp>/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).
+4
View File
@@ -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/<timestamp>/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/<timestamp>/`.
Dépendances optionnelles:
- `pip install pyyaml` pour `run_content_checks.sh`
- `pip install pyserial` pour suites USB, UI Link sim et console série
+9 -11
View File
@@ -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": []
}
]
@@ -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
}
+39 -2
View File
@@ -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 <command>"
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
+228 -22
View File
@@ -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
+130 -38
View File
@@ -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 <command> [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
+2
View File
@@ -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
+100 -3
View File
@@ -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"]:
+52
View File
@@ -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