Visual audit captures for all major pages (desktop, tablet, mobile). Add run-audit.sh and generate_page_fix_prompts.sh helper scripts. Add prompt templates directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
502 lines
17 KiB
Bash
Executable file
502 lines
17 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# VEZA — Script d'audit automatique overnight
|
||
# Lance Claude Code sur chaque prompt d'audit, avec reprise sur erreur.
|
||
#
|
||
# Usage:
|
||
# ./run-audit.sh # Lance depuis le début (ou reprend)
|
||
# ./run-audit.sh --reset # Repart de zéro
|
||
# ./run-audit.sh --from 15 # Force le démarrage au prompt 15
|
||
# ./run-audit.sh --only 38 # N'exécute qu'un seul prompt
|
||
# ./run-audit.sh --dry-run # Affiche ce qui serait fait sans exécuter
|
||
# ./run-audit.sh --status # Affiche l'état actuel
|
||
# ./run-audit.sh --model sonnet # Utilise Sonnet au lieu d'Opus (moins cher)
|
||
#
|
||
# Coût estimé (Opus): ~$150–400 pour les 58 prompts
|
||
# Coût estimé (Sonnet): ~$30–80 pour les 58 prompts
|
||
# Durée estimée: 4–8 heures selon le modèle et la charge API
|
||
#
|
||
set -euo pipefail
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Configuration
|
||
# ─────────────────────────────────────────────
|
||
|
||
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||
PROMPTS_DIR="$REPO_ROOT/prompts"
|
||
STATE_DIR="$REPO_ROOT/.audit-state"
|
||
STATE_FILE="$STATE_DIR/progress.json"
|
||
LOG_DIR="$STATE_DIR/logs"
|
||
BRANCH_NAME="audit/full-page-audit-$(date +%Y%m%d)"
|
||
|
||
# Claude CLI settings
|
||
MODEL="${VEZA_AUDIT_MODEL:-opus}"
|
||
MAX_BUDGET_PER_PROMPT="${VEZA_AUDIT_BUDGET_PER_PROMPT:-8}" # USD par prompt
|
||
MAX_RETRIES=3
|
||
RETRY_BACKOFF_BASE=60 # secondes, doublé à chaque retry
|
||
COOLDOWN_BETWEEN_PROMPTS=10 # secondes entre chaque prompt
|
||
RATE_LIMIT_PAUSE=300 # 5 min de pause si rate limit détecté
|
||
|
||
# Couleurs
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Fonctions utilitaires
|
||
# ─────────────────────────────────────────────
|
||
|
||
log() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; }
|
||
info() { echo -e "${GREEN}[$(date '+%H:%M:%S')] ✓${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] ⚠${NC} $*"; }
|
||
error() { echo -e "${RED}[$(date '+%H:%M:%S')] ✗${NC} $*"; }
|
||
|
||
die() { error "$*"; exit 1; }
|
||
|
||
# ─────────────────────────────────────────────
|
||
# State management (fichier JSON simple via jq)
|
||
# ─────────────────────────────────────────────
|
||
|
||
ensure_deps() {
|
||
command -v claude >/dev/null 2>&1 || die "claude CLI non trouvé. Installe-le d'abord."
|
||
command -v jq >/dev/null 2>&1 || die "jq non trouvé. Installe: sudo dnf install jq"
|
||
command -v git >/dev/null 2>&1 || die "git non trouvé."
|
||
}
|
||
|
||
init_state() {
|
||
mkdir -p "$STATE_DIR" "$LOG_DIR"
|
||
|
||
if [[ ! -f "$STATE_FILE" ]]; then
|
||
cat > "$STATE_FILE" <<'ENDJSON'
|
||
{
|
||
"started_at": null,
|
||
"branch": null,
|
||
"prompts": {},
|
||
"current": null,
|
||
"total_cost_estimate_usd": 0,
|
||
"total_prompts": 0,
|
||
"completed": 0,
|
||
"failed": 0,
|
||
"skipped": 0
|
||
}
|
||
ENDJSON
|
||
log "State file initialisé: $STATE_FILE"
|
||
fi
|
||
}
|
||
|
||
state_get() {
|
||
jq -r "$1" "$STATE_FILE"
|
||
}
|
||
|
||
state_set() {
|
||
local tmp
|
||
tmp=$(mktemp)
|
||
jq "$1" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
|
||
}
|
||
|
||
get_prompt_status() {
|
||
local num="$1"
|
||
jq -r ".prompts[\"$num\"].status // \"pending\"" "$STATE_FILE"
|
||
}
|
||
|
||
set_prompt_status() {
|
||
local num="$1" status="$2" detail="${3:-}"
|
||
state_set ".prompts[\"$num\"] = {
|
||
\"status\": \"$status\",
|
||
\"updated_at\": \"$(date -Iseconds)\",
|
||
\"detail\": \"$detail\"
|
||
}"
|
||
# Update counters
|
||
local completed failed
|
||
completed=$(jq '[.prompts[] | select(.status == "done")] | length' "$STATE_FILE")
|
||
failed=$(jq '[.prompts[] | select(.status == "failed")] | length' "$STATE_FILE")
|
||
state_set ".completed = $completed | .failed = $failed"
|
||
}
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Git helpers
|
||
# ─────────────────────────────────────────────
|
||
|
||
setup_branch() {
|
||
cd "$REPO_ROOT"
|
||
local current_branch
|
||
current_branch=$(git branch --show-current)
|
||
local existing_branch
|
||
existing_branch=$(state_get '.branch // empty')
|
||
|
||
if [[ -n "$existing_branch" ]] && git rev-parse --verify "$existing_branch" >/dev/null 2>&1; then
|
||
# Reprendre sur la branche existante
|
||
if [[ "$current_branch" != "$existing_branch" ]]; then
|
||
log "Reprise sur branche existante: $existing_branch"
|
||
git checkout "$existing_branch"
|
||
fi
|
||
BRANCH_NAME="$existing_branch"
|
||
else
|
||
# Créer nouvelle branche depuis l'état actuel
|
||
if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then
|
||
BRANCH_NAME="${BRANCH_NAME}-$(date +%H%M%S)"
|
||
fi
|
||
log "Création branche: $BRANCH_NAME"
|
||
git checkout -b "$BRANCH_NAME"
|
||
state_set ".branch = \"$BRANCH_NAME\""
|
||
fi
|
||
|
||
state_set ".started_at = \"$(date -Iseconds)\""
|
||
}
|
||
|
||
commit_prompt_changes() {
|
||
local num="$1" name="$2" path="$3"
|
||
cd "$REPO_ROOT"
|
||
|
||
# Vérifier s'il y a des changements
|
||
if git diff --quiet && git diff --cached --quiet && [[ -z "$(git ls-files --others --exclude-standard)" ]]; then
|
||
warn "Prompt $num ($path): aucun changement à committer"
|
||
return 0
|
||
fi
|
||
|
||
# Stage tous les changements (sauf les fichiers d'état de l'audit)
|
||
git add -A -- ':!.audit-state'
|
||
|
||
# Vérifier à nouveau après staging
|
||
if git diff --cached --quiet; then
|
||
warn "Prompt $num ($path): rien dans le staging"
|
||
return 0
|
||
fi
|
||
|
||
git commit -m "$(cat <<EOF
|
||
audit($path): fix bugs and add e2e tests — $name
|
||
|
||
Automated audit via Claude Code + Playwright MCP.
|
||
- Full page audit: loading, features, a11y, i18n, responsive, security
|
||
- Bug fixes applied
|
||
- E2E regression tests added
|
||
|
||
Prompt: prompts/$(printf '%02d' "$num")-*.md
|
||
EOF
|
||
)"
|
||
|
||
info "Commit créé pour prompt $num ($path)"
|
||
}
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Exécution d'un prompt
|
||
# ─────────────────────────────────────────────
|
||
|
||
run_single_prompt() {
|
||
local prompt_file="$1"
|
||
local num
|
||
num=$(basename "$prompt_file" | grep -oP '^\d+')
|
||
# Enlever le zéro devant pour le numéro
|
||
num=$((10#$num))
|
||
local log_file="$LOG_DIR/prompt-$(printf '%02d' "$num").log"
|
||
local route_name
|
||
route_name=$(basename "$prompt_file" .md | sed 's/^[0-9]*-//')
|
||
|
||
log "━━━ Prompt $num/58: $route_name ━━━"
|
||
|
||
local status
|
||
status=$(get_prompt_status "$num")
|
||
if [[ "$status" == "done" ]]; then
|
||
info "Déjà complété, skip"
|
||
return 0
|
||
fi
|
||
|
||
set_prompt_status "$num" "running"
|
||
state_set ".current = $num"
|
||
|
||
local prompt_content
|
||
prompt_content=$(cat "$prompt_file")
|
||
|
||
local attempt=0
|
||
local success=false
|
||
|
||
while (( attempt < MAX_RETRIES )); do
|
||
attempt=$((attempt + 1))
|
||
log "Tentative $attempt/$MAX_RETRIES..."
|
||
|
||
local exit_code=0
|
||
|
||
# Exécuter Claude Code
|
||
# --print: mode non-interactif
|
||
# --dangerously-skip-permissions: aucune interaction
|
||
# --max-budget-usd: garde-fou coût par prompt
|
||
timeout 1800 claude \
|
||
-p \
|
||
--dangerously-skip-permissions \
|
||
--model "$MODEL" \
|
||
--max-budget-usd "$MAX_BUDGET_PER_PROMPT" \
|
||
"$prompt_content" \
|
||
> "$log_file" 2>&1 \
|
||
|| exit_code=$?
|
||
|
||
if [[ $exit_code -eq 0 ]]; then
|
||
success=true
|
||
break
|
||
fi
|
||
|
||
# Analyser l'erreur
|
||
local last_lines
|
||
last_lines=$(tail -20 "$log_file" 2>/dev/null || echo "")
|
||
|
||
if echo "$last_lines" | grep -qi "rate.limit\|429\|too many requests\|overloaded"; then
|
||
warn "Rate limit détecté. Pause de ${RATE_LIMIT_PAUSE}s..."
|
||
sleep "$RATE_LIMIT_PAUSE"
|
||
elif echo "$last_lines" | grep -qi "context.window\|token.limit\|max.*token\|session.*limit"; then
|
||
warn "Limite de session/contexte atteinte. Prompt trop lourd, on continue."
|
||
break
|
||
elif echo "$last_lines" | grep -qi "network\|connection\|ECONNRESET\|ETIMEDOUT\|fetch failed"; then
|
||
local backoff=$((RETRY_BACKOFF_BASE * (2 ** (attempt - 1))))
|
||
warn "Erreur réseau. Backoff ${backoff}s..."
|
||
sleep "$backoff"
|
||
elif [[ $exit_code -eq 124 ]]; then
|
||
warn "Timeout (30min). Le prompt a pris trop de temps."
|
||
break
|
||
else
|
||
local backoff=$((RETRY_BACKOFF_BASE * (2 ** (attempt - 1))))
|
||
warn "Erreur inconnue (code $exit_code). Backoff ${backoff}s..."
|
||
sleep "$backoff"
|
||
fi
|
||
done
|
||
|
||
if $success; then
|
||
# Commit les changements
|
||
commit_prompt_changes "$num" "$route_name" "$(head -1 "$prompt_file" | grep -oP '/[a-z/:-]+')" || true
|
||
set_prompt_status "$num" "done"
|
||
info "Prompt $num terminé avec succès"
|
||
else
|
||
set_prompt_status "$num" "failed" "after $attempt attempts, exit code $exit_code"
|
||
error "Prompt $num échoué après $attempt tentatives"
|
||
# On continue quand même avec le suivant
|
||
fi
|
||
|
||
# Cooldown entre les prompts
|
||
if (( COOLDOWN_BETWEEN_PROMPTS > 0 )); then
|
||
log "Cooldown ${COOLDOWN_BETWEEN_PROMPTS}s..."
|
||
sleep "$COOLDOWN_BETWEEN_PROMPTS"
|
||
fi
|
||
}
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Collecte des prompts
|
||
# ─────────────────────────────────────────────
|
||
|
||
get_prompt_files() {
|
||
find "$PROMPTS_DIR" -name '*.md' -not -name 'ALL-PROMPTS.md' | sort
|
||
}
|
||
|
||
# ─────────────────────────────────────────────
|
||
# Commandes principales
|
||
# ─────────────────────────────────────────────
|
||
|
||
cmd_status() {
|
||
if [[ ! -f "$STATE_FILE" ]]; then
|
||
log "Aucun audit en cours."
|
||
return
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${CYAN}═══ VEZA Audit Status ═══${NC}"
|
||
echo ""
|
||
echo -e " Branche: $(state_get '.branch // "non démarrée"')"
|
||
echo -e " Démarré: $(state_get '.started_at // "—"')"
|
||
echo -e " Complétés: ${GREEN}$(state_get '.completed')${NC}"
|
||
echo -e " Échoués: ${RED}$(state_get '.failed')${NC}"
|
||
echo -e " En cours: $(state_get '.current // "—"')"
|
||
echo ""
|
||
|
||
# Détail par prompt
|
||
local total=0 done=0 failed=0 pending=0
|
||
for f in $(get_prompt_files); do
|
||
total=$((total + 1))
|
||
local num
|
||
num=$((10#$(basename "$f" | grep -oP '^\d+')))
|
||
local st
|
||
st=$(get_prompt_status "$num")
|
||
local icon="⏳"
|
||
case "$st" in
|
||
done) icon="✅"; done=$((done + 1)) ;;
|
||
failed) icon="❌"; failed=$((failed + 1)) ;;
|
||
running) icon="🔄" ;;
|
||
*) pending=$((pending + 1)) ;;
|
||
esac
|
||
printf " %s %02d. %s\n" "$icon" "$num" "$(basename "$f" .md | sed 's/^[0-9]*-//')"
|
||
done
|
||
|
||
echo ""
|
||
echo -e " Total: $total | ✅ $done | ❌ $failed | ⏳ $pending"
|
||
echo ""
|
||
}
|
||
|
||
cmd_reset() {
|
||
warn "Reset de l'état d'audit..."
|
||
rm -rf "$STATE_DIR"
|
||
info "État supprimé. Relance ./run-audit.sh pour recommencer."
|
||
}
|
||
|
||
cmd_run() {
|
||
local start_from="${1:-0}"
|
||
local only="${2:-0}"
|
||
|
||
ensure_deps
|
||
init_state
|
||
|
||
local prompt_files
|
||
mapfile -t prompt_files < <(get_prompt_files)
|
||
local total=${#prompt_files[@]}
|
||
|
||
state_set ".total_prompts = $total"
|
||
|
||
log "═══════════════════════════════════════════════════════"
|
||
log " VEZA — Audit automatique de $total pages"
|
||
log " Modèle: $MODEL"
|
||
log " Budget max par prompt: \$${MAX_BUDGET_PER_PROMPT}"
|
||
log " Budget max total estimé: \$$(echo "$total * $MAX_BUDGET_PER_PROMPT" | bc)"
|
||
log "═══════════════════════════════════════════════════════"
|
||
|
||
setup_branch
|
||
|
||
for prompt_file in "${prompt_files[@]}"; do
|
||
local num
|
||
num=$((10#$(basename "$prompt_file" | grep -oP '^\d+')))
|
||
|
||
# --from: sauter les prompts avant
|
||
if (( start_from > 0 && num < start_from )); then
|
||
continue
|
||
fi
|
||
|
||
# --only: n'exécuter qu'un seul
|
||
if (( only > 0 && num != only )); then
|
||
continue
|
||
fi
|
||
|
||
run_single_prompt "$prompt_file"
|
||
done
|
||
|
||
state_set ".current = null"
|
||
|
||
# Résumé final
|
||
echo ""
|
||
log "═══════════════════════════════════════════════════════"
|
||
log " AUDIT TERMINÉ"
|
||
log "═══════════════════════════════════════════════════════"
|
||
cmd_status
|
||
|
||
local failed_count
|
||
failed_count=$(state_get '.failed')
|
||
if (( failed_count > 0 )); then
|
||
warn "$failed_count prompts ont échoué. Relance pour retenter uniquement les échecs."
|
||
warn "Les logs sont dans: $LOG_DIR/"
|
||
fi
|
||
|
||
info "Branche: $BRANCH_NAME"
|
||
info "Commits: $(git log --oneline "$BRANCH_NAME" --not main | wc -l) commits"
|
||
info "Logs: $LOG_DIR/"
|
||
}
|
||
|
||
cmd_dry_run() {
|
||
init_state
|
||
local prompt_files
|
||
mapfile -t prompt_files < <(get_prompt_files)
|
||
|
||
echo ""
|
||
log "═══ DRY RUN — Voici ce qui serait exécuté ═══"
|
||
echo ""
|
||
|
||
for prompt_file in "${prompt_files[@]}"; do
|
||
local num
|
||
num=$((10#$(basename "$prompt_file" | grep -oP '^\d+')))
|
||
local st
|
||
st=$(get_prompt_status "$num")
|
||
local action="EXÉCUTER"
|
||
[[ "$st" == "done" ]] && action="SKIP (déjà fait)"
|
||
printf " %02d. %-40s → %s\n" "$num" "$(basename "$prompt_file")" "$action"
|
||
done
|
||
|
||
local pending
|
||
pending=$(jq '[.prompts[] | select(.status == "done")] | length' "$STATE_FILE" 2>/dev/null || echo 0)
|
||
local total=${#prompt_files[@]}
|
||
local to_run=$((total - pending))
|
||
|
||
echo ""
|
||
echo " À exécuter: $to_run / $total prompts"
|
||
echo " Modèle: $MODEL"
|
||
echo " Budget max: \$$(echo "$to_run * $MAX_BUDGET_PER_PROMPT" | bc)"
|
||
echo " Durée estimée: $(echo "$to_run * 8" | bc) min (≈$(echo "$to_run * 8 / 60" | bc)h)"
|
||
echo ""
|
||
}
|
||
|
||
# ─────────────────────────────────────────────
|
||
# CLI parsing
|
||
# ─────────────────────────────────────────────
|
||
|
||
main() {
|
||
cd "$REPO_ROOT"
|
||
|
||
local start_from=0
|
||
local only=0
|
||
local dry_run=false
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--reset)
|
||
cmd_reset
|
||
exit 0
|
||
;;
|
||
--status)
|
||
cmd_status
|
||
exit 0
|
||
;;
|
||
--dry-run)
|
||
dry_run=true
|
||
shift
|
||
;;
|
||
--from)
|
||
start_from="$2"
|
||
shift 2
|
||
;;
|
||
--only)
|
||
only="$2"
|
||
shift 2
|
||
;;
|
||
--model)
|
||
MODEL="$2"
|
||
shift 2
|
||
;;
|
||
--budget)
|
||
MAX_BUDGET_PER_PROMPT="$2"
|
||
shift 2
|
||
;;
|
||
-h|--help)
|
||
echo "Usage: $0 [options]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --reset Supprime l'état et repart de zéro"
|
||
echo " --status Affiche la progression actuelle"
|
||
echo " --dry-run Montre ce qui serait fait sans exécuter"
|
||
echo " --from N Commence au prompt N"
|
||
echo " --only N N'exécute que le prompt N"
|
||
echo " --model MODEL Modèle Claude (opus|sonnet|haiku) [défaut: opus]"
|
||
echo " --budget N Budget max USD par prompt [défaut: 8]"
|
||
echo ""
|
||
echo "Variables d'environnement:"
|
||
echo " VEZA_AUDIT_MODEL Modèle par défaut"
|
||
echo " VEZA_AUDIT_BUDGET_PER_PROMPT Budget par prompt"
|
||
exit 0
|
||
;;
|
||
*)
|
||
die "Option inconnue: $1 (utilise --help)"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if $dry_run; then
|
||
cmd_dry_run
|
||
else
|
||
cmd_run "$start_from" "$only"
|
||
fi
|
||
}
|
||
|
||
main "$@"
|