repowarden 4.12.0 → 4.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agents CHANGED
@@ -7,19 +7,22 @@
7
7
  # Sans --claude ni --opencode : les deux. --projet (défaut) : fichiers du dépôt,
8
8
  # à versionner ; --global : configuration de l'utilisateur, là où chaque outil
9
9
  # la lit (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME).
10
- # Relançable : fichiers existants complétés, jamais écrasés ; retirer n'enlève
11
- # que ce qu'init a écrit.
10
+ # Relançable : fichiers existants complétés ; un garde-fou posé par une version
11
+ # antérieure de repowarden est mis à jour, un fichier personnel jamais écrasé ;
12
+ # retirer n'enlève que ce qu'init a écrit.
12
13
  set -euo pipefail
13
14
 
14
15
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
15
16
  # shellcheck source=../hooks/lib/common.sh
16
17
  source "$ROOT/hooks/lib/common.sh"
18
+ # shellcheck source=../hooks/lib/agents.sh
19
+ source "$ROOT/hooks/lib/agents.sh"
17
20
 
18
21
  aide() { _tr help.agents; printf '%b\n' "$_T"; }
19
22
 
20
- MODELE_OPENCODE="$ROOT/hooks/agents/repowarden-garde.js"
21
- COMMANDE_CLAUDE="repowarden garde --format claude"
22
- MATCHER_CLAUDE="Edit|Write|MultiEdit|NotebookEdit|Bash"
23
+ MODELE_OPENCODE="$AGENTS_MODELE_OPENCODE"
24
+ COMMANDE_CLAUDE="$AGENTS_COMMANDE_CLAUDE"
25
+ MATCHER_CLAUDE="$AGENTS_MATCHER_CLAUDE"
23
26
 
24
27
  action=""
25
28
  case "${1:-}" in
@@ -59,7 +62,8 @@ fi
59
62
  ecrits=()
60
63
 
61
64
  # Hook PreToolUse de Claude Code : ajouté au settings.json (créé au besoin),
62
- # rien si une commande « repowarden garde » y est déjà
65
+ # rien si une commande « repowarden garde » y est déjà ; d'une ancienne forme,
66
+ # remplacé (les autres hooks restent)
63
67
  init_claude() {
64
68
  local f="$fichier_claude" entree tmp
65
69
  has jq || { err_t agents.no_jq "$f"; return 1; }
@@ -67,6 +71,18 @@ init_claude() {
67
71
  '{matcher: $m, hooks: [{type: "command", command: $c}]}')"
68
72
  if [ -f "$f" ]; then
69
73
  jq -e . "$f" >/dev/null 2>&1 || { err_t agents.bad_json "Claude Code" "$f"; return 1; }
74
+ if agents_claude_perime "$f"; then
75
+ tmp="$(mktemp)"
76
+ jq --argjson e "$entree" 'def rw: (.command // "") | startswith("repowarden garde");
77
+ .hooks.PreToolUse = ([.hooks.PreToolUse[]
78
+ | if any(.hooks[]?; rw) then (.hooks |= map(select(rw | not))) | select(.hooks | length > 0) else . end]
79
+ + [$e])' "$f" >"$tmp"
80
+ cat "$tmp" >"$f"
81
+ rm -f "$tmp"
82
+ ecrits+=("$f")
83
+ ok_t agents.updated "Claude Code" "$f"
84
+ return 0
85
+ fi
70
86
  if jq -e '[.hooks.PreToolUse[]?.hooks[]?.command // empty | select(test("repowarden garde"))] | length > 0' "$f" >/dev/null; then
71
87
  ok_t agents.present "Claude Code" "$f"
72
88
  return 0
@@ -83,12 +99,17 @@ init_claude() {
83
99
  ok_t agents.added "Claude Code" "$f"
84
100
  }
85
101
 
86
- # Plugin opencode : copie du modèle ; un fichier différent est gardé et signalé
102
+ # Plugin opencode : copie du modèle ; celui d'une version antérieure de
103
+ # repowarden est remplacé, un fichier personnel gardé et signalé
87
104
  init_opencode() {
88
105
  local f="$fichier_opencode"
89
106
  if [ -f "$f" ]; then
90
107
  if cmp -s "$f" "$MODELE_OPENCODE"; then
91
108
  ok_t agents.present opencode "$f"
109
+ elif agents_plugin_repowarden "$f"; then
110
+ cp "$MODELE_OPENCODE" "$f"
111
+ ecrits+=("$f")
112
+ ok_t agents.updated opencode "$f"
92
113
  else
93
114
  attention_t agents.kept opencode "$f" "$MODELE_OPENCODE"
94
115
  fi
package/bin/garde CHANGED
@@ -324,6 +324,49 @@ verifier_gh() {
324
324
  ) || exit $?
325
325
  }
326
326
 
327
+ # Découpe $1 en une commande par ligne (REPLY) : seul le « & » d'une vraie
328
+ # redirection (N>&M, >&2, >&-, &>, &>>, non échappée) reste dans la commande,
329
+ # citée entière ; partout ailleurs il sépare. Dans le doute couper : au pire une
330
+ # commande de trop, jamais une masquée (true \>&git push -f lance git).
331
+ decouper_lignes() {
332
+ local s="$1" out="" c i j prec avant nl=$'\n'
333
+ for ((i = 0; i < ${#s}; i++)); do
334
+ c="${s:i:1}"
335
+ if [ "$c" != "&" ]; then
336
+ out="$out$c"
337
+ continue
338
+ fi
339
+ prec="" avant=""
340
+ [ "$i" -lt 1 ] || prec="${s:i-1:1}"
341
+ [ "$i" -lt 2 ] || avant="${s:i-2:1}"
342
+ # &> et &>> : le « & » lui-même non échappé
343
+ if [ "${s:i+1:1}" = ">" ] && [ "$prec" != "\\" ]; then
344
+ out="$out&"
345
+ continue
346
+ fi
347
+ # >&N, <&N, >&- : « > » ou « < » non échappé avant, un descripteur ou
348
+ # « - » après, puis une fin de mot
349
+ if { [ "$prec" = ">" ] || [ "$prec" = "<" ]; } && [ "$avant" != "\\" ]; then
350
+ j=$((i + 1))
351
+ if [ "${s:j:1}" = "-" ]; then
352
+ j=$((j + 1))
353
+ else
354
+ while [[ "${s:j:1}" == [0-9] ]]; do j=$((j + 1)); done
355
+ fi
356
+ if [ "$j" -gt $((i + 1)) ]; then
357
+ case "${s:j:1}" in
358
+ "" | " " | $'\t' | "$nl" | "&" | ";" | "|" | ")")
359
+ out="$out&"
360
+ continue
361
+ ;;
362
+ esac
363
+ fi
364
+ fi
365
+ out="$out$nl"
366
+ done
367
+ REPLY="$out"
368
+ }
369
+
327
370
  # Ligne de commande shell : chaque commande (&&, ||, ;, |, retour à la ligne),
328
371
  # « cd » suivi, variables d'environnement en tête ignorées (sauf
329
372
  # REPOWARDEN_TICKET, le ticket d'une écriture gh hors du circuit des PR)
@@ -336,7 +379,10 @@ verifier_shell() {
336
379
  lignes="${lignes//||/$nl}"
337
380
  lignes="${lignes//;/$nl}"
338
381
  lignes="${lignes//|/$nl}"
339
- lignes="${lignes//&/$nl}"
382
+ # Un « & » de redirection (2>&1, >&2, &>log…) n'est pas un séparateur : la
383
+ # commande citée dans le refus et celle proposée restent entières
384
+ decouper_lignes "$lignes"
385
+ lignes="$REPLY"
340
386
  # Plusieurs commandes : un refus vaut pour toute la ligne (dit à l'agent)
341
387
  [ "$(grep -c '[^[:space:]]' <<<"$lignes")" -le 1 ] || LIGNE_MULTIPLE=1
342
388
  while IFS= read -r segment; do
package/bin/statut CHANGED
@@ -150,6 +150,13 @@ if agents_sans_garde "$GIT_TOPLEVEL"; then
150
150
  attention_t st.agents_sans_garde "$cmd_agents"
151
151
  suivant="${suivant:-$cmd_agents}"
152
152
  fi
153
+ # Garde-fou posé par une version antérieure de repowarden : init le met à jour
154
+ while read -r portee _agent fichier; do
155
+ cmd_agents="repowarden agents init"
156
+ [ "$portee" = projet ] || cmd_agents="$cmd_agents --global"
157
+ attention_t st.agents_stale "$fichier" "$cmd_agents"
158
+ suivant="${suivant:-$cmd_agents}"
159
+ done < <(agents_perimes "$GIT_TOPLEVEL")
153
160
 
154
161
  # Outils : secrets, code mort (selon les langages)
155
162
  presents="" absents=""
@@ -63,13 +63,14 @@ statut() {
63
63
  PIED="<!-- repowarden:pied -->"
64
64
 
65
65
  # REPLY = texte $1 terminé par le pied (---, puis une phrase en Markdown
66
- # normal : <sub> affiche les liens en brut) ; sans slug (jeton des Actions,
67
- # github-actions[bot]) ou déjà signé, texte inchangé
66
+ # normal : <sub> affiche les liens en brut). Le repère est seul sur sa ligne
67
+ # pour que GitHub rende les liens du pied en HTML (bloc HTML brut sinon) ;
68
+ # sans slug (jeton des Actions, github-actions[bot]) ou déjà signé, texte inchangé
68
69
  pied_r() {
69
70
  REPLY="$1"
70
71
  [ -n "${APP_SLUG:-}" ] && [[ "$REPLY" != *"$PIED"* ]] || return 0
71
72
  _tr tk.footer "$APP_SLUG"
72
- REPLY="$REPLY"$'\n\n---\n\n'"$PIED$_T"
73
+ REPLY="$REPLY"$'\n\n---\n\n'"$PIED"$'\n\n'"$_T"
73
74
  }
74
75
 
75
76
  # Commentaire $2 du bot sur le ticket $1, avec son pied
@@ -1,20 +1,75 @@
1
1
  #!/usr/bin/env bash
2
- # Agents IA du poste et garde-fou repowarden (repowarden statut, install.sh).
3
- # Compatible bash 3.2.
2
+ # Agents IA du poste et garde-fou repowarden (repowarden agents, statut,
3
+ # install.sh). Compatible bash 3.2.
4
+
5
+ # Forme actuelle du garde-fou : un garde-fou posé par repowarden qui en diffère
6
+ # date d'une version antérieure et est mis à jour (#470)
7
+ AGENTS_MODELE_OPENCODE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../agents" && pwd -P)/repowarden-garde.js"
8
+ AGENTS_COMMANDE_CLAUDE="repowarden garde --format claude"
9
+ AGENTS_MATCHER_CLAUDE="Edit|Write|MultiEdit|NotebookEdit|Bash"
10
+
11
+ # Dossiers de configuration de l'utilisateur, là où chaque outil les lit
12
+ # (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME)
13
+ agents_dossiers_globaux() {
14
+ AGENTS_CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
15
+ AGENTS_OPENCODE_DIR="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}"
16
+ }
17
+
18
+ # Vrai si le plugin opencode $1 a été posé par repowarden (en-tête du modèle,
19
+ # ancien plugin autonome compris) : lui seul peut être remplacé sans perte
20
+ agents_plugin_repowarden() {
21
+ local ligne=""
22
+ [ -f "$1" ] || return 1
23
+ IFS= read -r ligne <"$1" || true
24
+ case "$ligne" in "// Garde-fou repowarden pour opencode"*) return 0 ;; esac
25
+ return 1
26
+ }
27
+
28
+ # Vrai si le plugin $1 vient de repowarden mais diffère du modèle actuel :
29
+ # posé par une version antérieure, il n'a pas suivi les mises à jour
30
+ agents_plugin_perime() {
31
+ agents_plugin_repowarden "$1" && ! cmp -s "$1" "$AGENTS_MODELE_OPENCODE"
32
+ }
33
+
34
+ # Vrai si le settings.json $1 a un hook « repowarden garde… » d'une autre forme
35
+ # que l'actuelle (commande ou matcher) : posé par une version antérieure
36
+ agents_claude_perime() {
37
+ [ -f "$1" ] && command -v jq >/dev/null 2>&1 || return 1
38
+ jq -e --arg c "$AGENTS_COMMANDE_CLAUDE" --arg m "$AGENTS_MATCHER_CLAUDE" '
39
+ [.hooks.PreToolUse[]? | . as $e | .hooks[]?
40
+ | select((.command // "") | startswith("repowarden garde"))
41
+ | select(.command != $c or $e.matcher != $m)] | length > 0' "$1" >/dev/null 2>&1
42
+ }
43
+
44
+ # Garde-fous périmés du dépôt $1 (vide hors dépôt) puis de l'utilisateur, une
45
+ # ligne « portée agent fichier » chacun : statut les signale, install les met à jour
46
+ agents_perimes() {
47
+ agents_dossiers_globaux
48
+ if [ -n "${1:-}" ]; then
49
+ ! agents_claude_perime "$1/.claude/settings.json" || echo "projet claude $1/.claude/settings.json"
50
+ ! agents_plugin_perime "$1/.opencode/plugins/repowarden-garde.js" ||
51
+ echo "projet opencode $1/.opencode/plugins/repowarden-garde.js"
52
+ fi
53
+ ! agents_claude_perime "$AGENTS_CLAUDE_DIR/settings.json" || echo "global claude $AGENTS_CLAUDE_DIR/settings.json"
54
+ ! agents_plugin_perime "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js" ||
55
+ echo "global opencode $AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js"
56
+ return 0
57
+ }
4
58
 
5
59
  # Vrai si Claude Code ou opencode est sur ce poste (commande, ou dossier de
6
60
  # configuration) sans garde-fou branché, ni dans le dépôt $1 (racine, vide hors
7
61
  # dépôt) ni pour l'utilisateur : ses actions ne seraient vues qu'au commit
8
62
  agents_sans_garde() {
9
- local claude_dir opencode_dir f fichiers=()
10
- claude_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
11
- opencode_dir="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}"
63
+ local f fichiers=()
64
+ agents_dossiers_globaux
12
65
  command -v claude >/dev/null 2>&1 || command -v opencode >/dev/null 2>&1 ||
13
- [ -d "$claude_dir" ] || [ -d "$opencode_dir" ] || return 1
66
+ [ -d "$AGENTS_CLAUDE_DIR" ] || [ -d "$AGENTS_OPENCODE_DIR" ] || return 1
14
67
  [ -z "${1:-}" ] || fichiers=("$1/.claude/settings.json" "$1/.opencode/plugins/repowarden-garde.js")
15
- fichiers+=("$claude_dir/settings.json" "$opencode_dir/plugins/repowarden-garde.js")
68
+ fichiers+=("$AGENTS_CLAUDE_DIR/settings.json" "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js")
69
+ # Plugin repowarden d'une version antérieure : branché (périmé, pas absent)
16
70
  for f in "${fichiers[@]}"; do
17
71
  ! grep -qs "repowarden garde" "$f" || return 1
72
+ ! agents_plugin_repowarden "$f" || return 1
18
73
  done
19
74
  return 0
20
75
  }
@@ -512,6 +512,7 @@ _msg_en() {
512
512
  st.wf_fix) _M="fix the uses: lines reported above" ;;
513
513
  st.tools) _M="Tools present: %s · missing: %s (depending on the projects' languages)" ;;
514
514
  st.agents_sans_garde) _M="AI agent without the guard (Claude Code, opencode): its actions are only checked at commit time. → %s" ;;
515
+ st.agents_stale) _M="Outdated repowarden guard (%s): wired by an earlier version of repowarden. → %s" ;;
515
516
  st.next) _M="Next step:" ;;
516
517
  st.ready) _M="All set: git cc to commit, repowarden code-mort on a branch." ;;
517
518
  st.help) _M="Every command: repowarden --help" ;;
@@ -742,6 +743,7 @@ _msg_en() {
742
743
  garde.gh_ticket_non_valide) _M="command refused: \"%s\"; ticket #%s is not open and validated (label \"%s\").\n → Get the ticket validated by the maintainer, then run again." ;;
743
744
  agents.added) _M="%s: guard added (%s)." ;;
744
745
  agents.present) _M="%s: guard already in place (%s)." ;;
746
+ agents.updated) _M="%s: guard updated (%s)." ;;
745
747
  agents.kept) _M="%s: %s exists and differs from the template; kept as is (template: %s)." ;;
746
748
  agents.bad_json) _M="%s: %s is not valid JSON; nothing was changed." ;;
747
749
  agents.no_jq) _M="jq is required to update %s (https://jqlang.org)." ;;
@@ -513,6 +513,7 @@ _msg_fr() {
513
513
  st.wf_fix) _M="corriger les lignes uses: signalées ci-dessus" ;;
514
514
  st.tools) _M="Outils présents : %s · absents : %s (selon les langages des projets)" ;;
515
515
  st.agents_sans_garde) _M="Agent IA sans garde-fou (Claude Code, opencode) : ses actions ne sont vérifiées qu'au commit. → %s" ;;
516
+ st.agents_stale) _M="Garde-fou repowarden périmé (%s) : posé par une version antérieure de repowarden. → %s" ;;
516
517
  st.next) _M="Étape suivante :" ;;
517
518
  st.ready) _M="Tout est prêt : git cc pour commiter, repowarden code-mort sur une branche." ;;
518
519
  st.help) _M="Toutes les commandes : repowarden --help" ;;
@@ -743,6 +744,7 @@ _msg_fr() {
743
744
  garde.gh_ticket_non_valide) _M="commande refusée : « %s » ; le ticket #%s n'est pas ouvert et validé (étiquette « %s »).\n → Faire valider le ticket par le mainteneur, puis relancer." ;;
744
745
  agents.added) _M="%s : garde-fou ajouté (%s)." ;;
745
746
  agents.present) _M="%s : garde-fou déjà en place (%s)." ;;
747
+ agents.updated) _M="%s : garde-fou mis à jour (%s)." ;;
746
748
  agents.kept) _M="%s : %s existe et diffère du modèle ; gardé tel quel (modèle : %s)." ;;
747
749
  agents.bad_json) _M="%s : %s n'est pas un JSON valide ; rien n'a été modifié." ;;
748
750
  agents.no_jq) _M="jq est requis pour compléter %s (https://jqlang.org)." ;;
package/install.sh CHANGED
@@ -253,6 +253,17 @@ if [ "$SCOPE" = --global ]; then
253
253
  info "$REPLY"
254
254
  fi
255
255
 
256
+ # Garde-fou de l'utilisateur posé par une version antérieure : mis à jour sans
257
+ # question (déjà branché par lui), une ligne par agent
258
+ while read -r portee agent fichier; do
259
+ [ "$portee" = global ] || continue
260
+ bash "$ROOT/bin/agents" init --global "--$agent" >/dev/null 2>&1 || continue
261
+ nom=opencode
262
+ [ "$agent" != claude ] || nom="Claude Code"
263
+ t agents.updated "$nom" "$fichier"
264
+ ok "$REPLY"
265
+ done < <(agents_perimes "")
266
+
256
267
  # Agent IA sans garde-fou (Claude Code, opencode) : branchement proposé dans un
257
268
  # terminal, jamais fait sans accord ; sinon, la commande sur une ligne
258
269
  RACINE="$(git rev-parse --show-toplevel 2>/dev/null || true)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.12.0",
3
+ "version": "4.12.1",
4
4
  "description": "Hooks git et CI multi-langages : messages de commit, nommage des branches, secrets, formatage, tests ciblés, code mort, releases.",
5
5
  "keywords": [
6
6
  "git-hooks",