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", "label": "Zacus: Firmware · Bootstrap venv",
"type": "shell", "type": "shell",
"command": "bash", "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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" }
}, },
@@ -15,8 +16,9 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" }
}, },
@@ -25,7 +27,7 @@
"type": "shell", "type": "shell",
"command": "bash", "command": "bash",
"args": ["-lc", "./tools/dev/zacus.sh rc"], "args": ["-lc", "./tools/dev/zacus.sh rc"],
"options": { "cwd": "${workspaceFolder}" }, "options": { "cwd": "${workspaceFolder}/hardware/firmware" },
"problemMatcher": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" }
}, },
@@ -37,18 +39,7 @@
"-lc", "-lc",
"./tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md" "./tools/dev/zacus.sh codex --prompt tools/dev/codex_prompts/zacus_overhaul_one_shot.md"
], ],
"options": { "cwd": "${workspaceFolder}" }, "options": { "cwd": "${workspaceFolder}/hardware/firmware" },
"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"
],
"problemMatcher": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" }
}, },
@@ -58,8 +49,9 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" }
}, },
@@ -69,8 +61,9 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus-watch", "reveal": "always" }
}, },
@@ -80,8 +73,9 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" }
}, },
@@ -90,8 +84,12 @@
"type": "shell", "type": "shell",
"command": "bash", "command": "bash",
"args": ["-lc", "echo \"Cockpit started (stop tasks to close watchers).\""], "args": ["-lc", "echo \"Cockpit started (stop tasks to close watchers).\""],
"options": { "cwd": "${workspaceFolder}/hardware/firmware" },
"dependsOrder": "parallel", "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": [], "problemMatcher": [],
"presentation": { "panel": "dedicated", "group": "zacus", "reveal": "always" } "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 - `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é) - `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. - `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 ## État du projet
- **Kit MJ** : complet (script, plan, antic-chaos, solution, checklists, export PDF). - **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 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: Dépendances optionnelles:
- `pip install pyyaml` pour `run_content_checks.sh` - `pip install pyyaml` pour `run_content_checks.sh`
- `pip install pyserial` pour suites USB, UI Link sim et console série - `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", "label": "Zacus: RC Live",
"type": "shell", "type": "shell",
"command": "bash", "command": "bash",
"args": [ "args": ["-lc", "set -euo pipefail; ./tools/dev/zacus.sh rc"],
"-lc",
"set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" rc"
],
"options": { "cwd": "${workspaceFolder}" }, "options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }, "presentation": { "panel": "dedicated", "reveal": "always" },
"problemMatcher": [] "problemMatcher": []
@@ -19,7 +16,7 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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}" }, "options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }, "presentation": { "panel": "dedicated", "reveal": "always" },
@@ -29,10 +26,7 @@
"label": "Zacus: RC Live + AutoFix", "label": "Zacus: RC Live + AutoFix",
"type": "shell", "type": "shell",
"command": "bash", "command": "bash",
"args": [ "args": ["-lc", "set -euo pipefail; ./tools/dev/zacus.sh rc-autofix"],
"-lc",
"set -euo pipefail; ROOT=$(git rev-parse --show-toplevel); \"$ROOT/tools/dev/zacus.sh\" rc-autofix"
],
"options": { "cwd": "${workspaceFolder}" }, "options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always" }, "presentation": { "panel": "dedicated", "reveal": "always" },
"problemMatcher": [] "problemMatcher": []
@@ -43,10 +37,14 @@
"command": "bash", "command": "bash",
"args": [ "args": [
"-lc", "-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}" }, "options": { "cwd": "${workspaceFolder}" },
"presentation": { "panel": "dedicated", "reveal": "always", "showReuseMessage": false }, "presentation": {
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": false
},
"problemMatcher": [] "problemMatcher": []
} }
] ]
@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# --- Codex Prompt Menu TUI ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel)" REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel)"
FW_ROOT="$REPO_ROOT/hardware/firmware" 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" ARTIFACTS_DIR="$FW_ROOT/artifacts/rc_live"
LAST_MESSAGE_FILE="$ARTIFACTS_DIR/_codex_last_message.md" 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() { collect_prompts() {
local glob local glob
prompt_files=() prompt_files=()
@@ -17,6 +28,8 @@ collect_prompts() {
done done
} }
# Menu interactif pour choisir un prompt (TUI si possible)
main_menu() { main_menu() {
collect_prompts collect_prompts
if [[ ${#prompt_files[@]} -eq 0 ]]; then if [[ ${#prompt_files[@]} -eq 0 ]]; then
@@ -24,25 +37,43 @@ main_menu() {
exit 1 exit 1
fi fi
while true; do while true; do
echo local choice
echo "Available prompts:" if [[ -n "$TUI_CMD" ]]; then
for idx in "${!prompt_files[@]}"; do local menu_args=()
printf "%2d) %s\n" $((idx + 1)) "$(basename "${prompt_files[idx]}")" for idx in "${!prompt_files[@]}"; do
done menu_args+=("$idx" "$(basename "${prompt_files[idx]}")")
read -rp "Choice (q to quit): " choice done
if [[ "$choice" =~ ^[Qq]$ ]]; then choice=$( \
exit 0 $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 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 done
} }
+39 -2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# --- Zacus.sh TUI ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ARTIFACT_ROOT="$REPO_ROOT/artifacts/rc_live" 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" RC_RUNNER="$REPO_ROOT/tools/dev/run_matrix_and_smoke.sh"
PROMPT_DIR="$SCRIPT_DIR/codex_prompts" 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() { require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then if ! command -v "$1" >/dev/null 2>&1; then
echo "[zacus] missing command: $1" >&2 echo "[zacus] missing command: $1" >&2
@@ -193,10 +204,36 @@ HELP
mkdir -p "$ARTIFACT_ROOT" mkdir -p "$ARTIFACT_ROOT"
# Menu interactif si aucun argument fourni
command=${1:-} command=${1:-}
if [[ -z "$command" ]]; then if [[ -z "$command" ]]; then
usage if [[ -n "$TUI_CMD" ]]; then
exit 1 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 fi
case "$command" in case "$command" in
+228 -22
View File
@@ -1,6 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git -C "$SCRIPT_DIR/../.." rev-parse --show-toplevel)" REPO_ROOT="$(git -C "$SCRIPT_DIR/../.." rev-parse --show-toplevel)"
FW_ROOT="$REPO_ROOT/hardware/firmware" 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" PROMPT_DIR="$FW_ROOT/tools/dev/codex_prompts"
RC_PROMPT="$PROMPT_DIR/rc_live_fail.prompt.md" 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() { run_bootstrap() {
(cd "$REPO_ROOT" && ./tools/dev/bootstrap_local.sh) "$FW_ROOT/tools/dev/bootstrap_local.sh"
} }
run_build_all() { run_build_all() {
(cd "$FW_ROOT" && ./build_all.sh) "$FW_ROOT/build_all.sh"
} }
run_rc_live() { run_rc_live() {
local artifacts="" 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)" artifacts="$(latest_artifacts)"
if [[ -n "$artifacts" && -f "$RC_PROMPT" ]]; then if [[ -n "$artifacts" && -f "$RC_PROMPT" ]]; then
ARTIFACT_PATH="$artifacts" codex exec - < "$RC_PROMPT" ARTIFACT_PATH="$artifacts" codex exec - < "$RC_PROMPT"
@@ -46,34 +74,212 @@ latest_artifacts() {
ls -1d "$ARTIFACTS_ROOT"/*/ 2>/dev/null | sort | tail -n1 ls -1d "$ARTIFACTS_ROOT"/*/ 2>/dev/null | sort | tail -n1
} }
menu() {
cat <<'EOF' # Affichage du menu (fzf > dialog > texte coloré)
Firmware cockpit show_menu() {
1) bootstrap (tools/dev/bootstrap_local.sh) local options=(
2) build all firmware (hardware/firmware/build_all.sh) "1:RC live gate (ZACUS_REQUIRE_HW unset)"
3) rc live gate (ZACUS_REQUIRE_HW unset) "2:RC live gate (ZACUS_REQUIRE_HW=1)"
4) rc live gate (ZACUS_REQUIRE_HW=1) "3:Build all firmware (hardware/firmware/build_all.sh)"
5) watch serial ports "4:Bootstrap (outils/dev/bootstrap_local.sh)"
6) run codex prompt menu "5:Watch serial ports"
0) exit "6:Run codex prompt menu"
EOF "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() { run_codex_prompts() {
"$FW_ROOT/tools/dev/codex_prompt_menu.sh" "$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 while true; do
menu echo "[DEBUG] Affichage du menu..." >&2
read -rp "Choice: " choice choice=$(show_menu)
echo "[DEBUG] Choix utilisateur: '$choice'" >&2
case "$choice" in case "$choice" in
1) run_bootstrap ;; 1) ZACUS_REQUIRE_HW=0 run_rc_live ;;
2) run_build_all ;; 2) ZACUS_REQUIRE_HW=1 run_rc_live ;;
3) ZACUS_REQUIRE_HW=0 run_rc_live ;; 3) run_build_all ;;
4) ZACUS_REQUIRE_HW=1 run_rc_live ;; 4) run_bootstrap ;;
5) ports_watch ;; 5) ports_watch ;;
6) run_codex_prompts ;; 6) run_codex_prompts ;;
0) exit 0 ;; 7) afficher_logs_menu ;;
*) echo "Unknown option: $choice" ;; 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 esac
done done
+130 -38
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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" FW_ROOT="$REPO_ROOT/hardware/firmware"
RC_RUNNER="$FW_ROOT/tools/dev/run_matrix_and_smoke.sh" RC_RUNNER="$FW_ROOT/tools/dev/run_matrix_and_smoke.sh"
SMOKE_SCRIPT="$FW_ROOT/tools/dev/serial_smoke.py" SMOKE_SCRIPT="$FW_ROOT/tools/dev/serial_smoke.py"
@@ -11,6 +11,7 @@ ARTIFACT_ROOT="$REPO_ROOT/artifacts"
PORTS_ARTIFACT_ROOT="$ARTIFACT_ROOT/ports" PORTS_ARTIFACT_ROOT="$ARTIFACT_ROOT/ports"
LATEST_PORTS_JSON="$PORTS_ARTIFACT_ROOT/latest_ports_resolve.json" LATEST_PORTS_JSON="$PORTS_ARTIFACT_ROOT/latest_ports_resolve.json"
CODEX_ARTIFACT_ROOT="$ARTIFACT_ROOT/codex" CODEX_ARTIFACT_ROOT="$ARTIFACT_ROOT/codex"
DEFAULT_PORTS_FIXTURE="$REPO_ROOT/tools/test/fixtures/ports_list_macos.txt"
LAST_RESOLVED_JSON="" LAST_RESOLVED_JSON=""
info() { info() {
@@ -22,15 +23,20 @@ die() {
exit 1 exit 1
} }
die_codex() {
printf '[zacus] ERROR: %s\n' "$*" >&2
exit 2
}
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
Usage: zacus.sh <command> [args] Usage: zacus.sh <command> [args]
Commands: 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) smoke Run the serial smoke helper (pass --role ... args)
ports Resolve ports once and show summary ports Resolve ports once and show summary
codex Invoke codex exec with a prompt (see --help) codex Invoke Codex CLI with a prompt
menu Show the zacus cockpit menu menu Show the Zacus cockpit menu
help Show this usage help Show this usage
USAGE USAGE
} }
@@ -41,24 +47,38 @@ print_ports_summary() {
printf '[ports] no resolved data yet (run "zacus.sh ports" to resolve)\n' printf '[ports] no resolved data yet (run "zacus.sh ports" to resolve)\n'
return 1 return 1
fi fi
python3 - <<'PY' python3 - "$json_file" <<'PY'
import datetime
import json import json
import pathlib import pathlib
import sys import sys
path = pathlib.Path(sys.argv[1]) path = pathlib.Path(sys.argv[1])
data = json.loads(path.read_text(encoding='utf-8')) try:
ports = data.get('ports', {}) data = json.loads(path.read_text(encoding='utf-8'))
reasons = data.get('reasons', {}) ports = data.get('ports', {})
notes = data.get('notes', []) reasons = data.get('reasons', {})
labels = [('esp32', 'esp32_audio'), ('esp8266', 'esp8266_oled')] notes = data.get('notes', [])
print(f'[ports] summary ({path.name}):') labels = [('esp32', 'esp32_audio'), ('esp8266', 'esp8266_oled')]
for role, label in labels: timestamp = path.parent.name
port = ports.get(role) or 'n/a' age = 'unknown'
reason = reasons.get(role) or 'unset' try:
print(f' - {label}: {port} (reason={reason})') dt = datetime.datetime.strptime(timestamp, '%Y%m%d-%H%M%S')
print(' - rp2040_tft: not auto-resolved (manual selection)') dt = dt.replace(tzinfo=datetime.timezone.utc)
print('[ports] notes:', ' | '.join(notes) if notes else 'none') 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 PY
} }
@@ -69,24 +89,64 @@ resolve_ports_once() {
local dir="$PORTS_ARTIFACT_ROOT/$timestamp" local dir="$PORTS_ARTIFACT_ROOT/$timestamp"
mkdir -p "$dir" mkdir -p "$dir"
local json="$dir/ports_resolve.json" 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)" info "resolving ports (output -> $json)"
set +e set +e
python3 "$REPO_ROOT/tools/test/resolve_ports.py" \ "${resolver_args[@]}"
--need-esp32 --need-esp8266 --ports-resolve-json "$json" --wait-port 3
local rc=$? local rc=$?
set -e set -e
if [[ -f "$json" ]]; then if [[ -f "$json" ]]; then
cp "$json" "$LATEST_PORTS_JSON" 2>/dev/null || true cp "$json" "$LATEST_PORTS_JSON" 2>/dev/null || true
LAST_RESOLVED_JSON="$json"
fi fi
LAST_RESOLVED_JSON="$json"
return $rc 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() { cmd_ports() {
resolve_ports_once || true resolve_ports_once || true
if [[ -n "${LAST_RESOLVED_JSON:-}" ]]; then local json_file=""
print_ports_summary "$LAST_RESOLVED_JSON" || true if [[ -n "${LAST_RESOLVED_JSON}" && -f "$LAST_RESOLVED_JSON" ]]; then
printf '[ports] data file: %s\n' "$LAST_RESOLVED_JSON" 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 else
printf '[ports] resolver did not produce a JSON file\n' printf '[ports] resolver did not produce a JSON file\n'
fi fi
@@ -107,10 +167,10 @@ cmd_smoke() {
ensure_codex_ready() { ensure_codex_ready() {
if ! command -v codex >/dev/null 2>&1; then 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 fi
if ! codex login status >/dev/null 2>&1; then 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 fi
} }
@@ -118,7 +178,6 @@ cmd_codex() {
local prompt_file="" local prompt_file=""
local sandbox="workspace-write" local sandbox="workspace-write"
local auto_mode="0" local auto_mode="0"
local custom_sandbox=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--prompt) --prompt)
@@ -154,16 +213,17 @@ HELP
fi fi
ensure_codex_ready ensure_codex_ready
resolve_ports_once || true resolve_ports_once || true
if [[ -n "${LAST_RESOLVED_JSON:-}" ]]; then local ports_json="${LAST_RESOLVED_JSON:-$LATEST_PORTS_JSON}"
print_ports_summary "$LAST_RESOLVED_JSON" || true if [[ -n "$ports_json" && -f "$ports_json" ]]; then
print_ports_summary "$ports_json" || true
fi fi
mkdir -p "$CODEX_ARTIFACT_ROOT" mkdir -p "$CODEX_ARTIFACT_ROOT"
local ts local ts
ts=$(date -u +%Y%m%d-%H%M%S) ts=$(date -u +%Y%m%d-%H%M%S)
local art_dir="$CODEX_ARTIFACT_ROOT/$ts" local art_dir="$CODEX_ARTIFACT_ROOT/$ts"
mkdir -p "$art_dir" mkdir -p "$art_dir"
if [[ -f "$LAST_RESOLVED_JSON" ]]; then if [[ -n "$ports_json" && -f "$ports_json" ]]; then
cp "$LAST_RESOLVED_JSON" "$art_dir/ports_resolve.json" 2>/dev/null || true cp "$ports_json" "$art_dir/ports_resolve.json" 2>/dev/null || true
fi fi
printf 'prompt=%s\nsandbox=%s\nauto=%s\n' "$prompt_file" "$sandbox" "$auto_mode" > "$art_dir/metadata.txt" printf 'prompt=%s\nsandbox=%s\nauto=%s\n' "$prompt_file" "$sandbox" "$auto_mode" > "$art_dir/metadata.txt"
local exec_cmd=(codex exec --sandbox "$sandbox") local exec_cmd=(codex exec --sandbox "$sandbox")
@@ -241,9 +301,29 @@ menu_run_codex() {
} }
cmd_menu() { 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 while true; do
printf '\n=== Zacus cockpit ===\n' printf '\n=== Zacus cockpit ===\n'
print_ports_summary "${LATEST_PORTS_JSON}" || true show_ports_context || true
printf '[menu] options:\n' printf '[menu] options:\n'
printf ' 1) RC live\n' printf ' 1) RC live\n'
printf ' 2) Serial smoke\n' printf ' 2) Serial smoke\n'
@@ -252,12 +332,24 @@ cmd_menu() {
printf ' 0) Exit\n' printf ' 0) Exit\n'
read -rp 'Choice: ' choice read -rp 'Choice: ' choice
case "$choice" in case "$choice" in
1) cmd_rc ;; 1)
2) cmd_smoke ;; cmd_rc
3) menu_run_codex ;; ;;
4) cmd_ports ;; 2)
0) return 0 ;; cmd_smoke
*) printf 'unknown choice\n' ;; ;;
3)
menu_run_codex
;;
4)
cmd_ports
;;
0)
return 0
;;
*)
printf 'unknown choice\n'
;;
esac esac
done done
} }
@@ -278,7 +370,7 @@ case "$command" in
cmd_codex "$@" cmd_codex "$@"
;; ;;
menu) menu)
cmd_menu cmd_menu "$@"
;; ;;
help|-h|--help) help|-h|--help)
usage 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 sys
import time import time
from pathlib import Path from pathlib import Path
import csv
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
PY_SERIAL_ERROR = ""
try: try:
import serial import serial
from serial.tools import list_ports from serial.tools import list_ports
except ImportError: except ImportError as exc:
print(json.dumps({"status": "fail", "notes": ["pyserial missing: pip install pyserial"]})) serial = None
raise SystemExit(2) list_ports = None
PY_SERIAL_ERROR = str(exc)
REPO_ROOT = Path(__file__).resolve().parents[2] 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_BAUDS = (115200, 19200)
FINGERPRINT_TIMEOUT = 2.0 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: def is_bluetooth_port(device: str) -> bool:
lower = device.lower() lower = device.lower()
@@ -65,6 +102,43 @@ def parse_location(port) -> str:
return "" 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]]: def load_map_file(path: Path) -> Dict[str, Dict[str, str]]:
data = {"location": {}, "vidpid": {}} data = {"location": {}, "vidpid": {}}
if not path.exists(): 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: def gather_ports(wait_port: int) -> List:
deadline = time.monotonic() + max(1, wait_port) deadline = time.monotonic() + max(1, wait_port)
if FIXTURE_PORTS:
return FIXTURE_PORTS
if list_ports is None:
return []
while True: while True:
ports = list(list_ports.comports()) ports = list(list_ports.comports())
if ports: 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("--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("--no-auto-ports", dest="auto_ports", action="store_false")
parser.add_argument("--prefer-cu", action="store_true") 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("--interactive", action="store_true")
parser.add_argument("--ports-resolve-json", default="") parser.add_argument("--ports-resolve-json", default="")
args = parser.parse_args() args = parser.parse_args()
@@ -430,6 +510,20 @@ def main() -> int:
args.port_esp8266 = env_esp8266 args.port_esp8266 = env_esp8266
env_override["esp8266"] = True 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() ports_map = load_ports_map()
initial_ports = {"esp32": args.port_esp32, "esp8266": args.port_esp8266} initial_ports = {"esp32": args.port_esp32, "esp8266": args.port_esp8266}
@@ -458,6 +552,9 @@ def main() -> int:
}, },
"notes": [], "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 = [] missing_roles = []
if args.need_esp32 and not result["ports"]["esp32"]: 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