repowarden 4.12.1 → 4.13.0-next.18

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
@@ -2,14 +2,15 @@
2
2
  # Branche le garde-fou repowarden (repowarden garde) dans les agents IA :
3
3
  # repowarden agents init [--claude] [--opencode] [--projet | --global]
4
4
  # repowarden agents retirer [--claude] [--opencode] [--projet | --global]
5
- # Claude Code hook PreToolUse dans .claude/settings.json
6
- # opencode plugin .opencode/plugins/repowarden-garde.js
5
+ # Claude Code hook PreToolUse dans .claude/settings.json + skill repowarden
6
+ # opencode plugins .opencode/plugins/repowarden-garde.js (opencode 1.x)
7
+ # et repowarden-garde-v2.js (opencode 2.x) + skill repowarden
7
8
  # Sans --claude ni --opencode : les deux. --projet (défaut) : fichiers du dépôt,
8
9
  # à versionner ; --global : configuration de l'utilisateur, là où chaque outil
9
10
  # la lit (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME).
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.
11
+ # Relançable : fichiers existants complétés ; un garde-fou ou une skill posés
12
+ # par une version antérieure de repowarden sont mis à jour, un fichier
13
+ # personnel jamais écrasé ; retirer n'enlève que ce qu'init a écrit.
13
14
  set -euo pipefail
14
15
 
15
16
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
@@ -20,9 +21,9 @@ source "$ROOT/hooks/lib/agents.sh"
20
21
 
21
22
  aide() { _tr help.agents; printf '%b\n' "$_T"; }
22
23
 
23
- MODELE_OPENCODE="$AGENTS_MODELE_OPENCODE"
24
24
  COMMANDE_CLAUDE="$AGENTS_COMMANDE_CLAUDE"
25
25
  MATCHER_CLAUDE="$AGENTS_MATCHER_CLAUDE"
26
+ MODELE_SKILL="$(agents_modele_skill)"
26
27
 
27
28
  action=""
28
29
  case "${1:-}" in
@@ -50,13 +51,17 @@ if [ "$portee" = projet ]; then
50
51
  [ -n "$GIT_TOPLEVEL" ] || { err_t install.not_repo; exit 1; }
51
52
  base="$GIT_TOPLEVEL"
52
53
  fichier_claude="$base/.claude/settings.json"
53
- fichier_opencode="$base/.opencode/plugins/repowarden-garde.js"
54
+ dossier_opencode="$base/.opencode/plugins"
55
+ fichier_skill_claude="$base/.claude/skills/repowarden/SKILL.md"
56
+ fichier_skill_opencode="$base/.opencode/skills/repowarden/SKILL.md"
54
57
  else
55
58
  # Réglages utilisateur de Claude Code : $CLAUDE_CONFIG_DIR s'il est défini
56
59
  # (code.claude.com/docs/en/settings) ; plugins globaux d'opencode :
57
60
  # $OPENCODE_CONFIG_DIR, sinon $XDG_CONFIG_HOME/opencode (opencode.ai/docs/config)
58
61
  fichier_claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
59
- fichier_opencode="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/plugins/repowarden-garde.js"
62
+ dossier_opencode="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/plugins"
63
+ fichier_skill_claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/repowarden/SKILL.md"
64
+ fichier_skill_opencode="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/skills/repowarden/SKILL.md"
60
65
  fi
61
66
 
62
67
  ecrits=()
@@ -99,26 +104,49 @@ init_claude() {
99
104
  ok_t agents.added "Claude Code" "$f"
100
105
  }
101
106
 
102
- # Plugin opencode : copie du modèle ; celui d'une version antérieure de
103
- # repowarden est remplacé, un fichier personnel gardé et signalé
107
+ # Plugins opencode (1.x et 2.x) : copie du modèle ; celui d'une version
108
+ # antérieure de repowarden est remplacé, un fichier personnel gardé et signalé
104
109
  init_opencode() {
105
- local f="$fichier_opencode"
106
- if [ -f "$f" ]; then
107
- if cmp -s "$f" "$MODELE_OPENCODE"; then
110
+ local p f modele
111
+ for p in $AGENTS_PLUGINS_OPENCODE; do
112
+ f="$dossier_opencode/$p" modele="$AGENTS_MODELES/$p"
113
+ if [ ! -f "$f" ]; then
114
+ mkdir -p "$dossier_opencode"
115
+ cp "$modele" "$f"
116
+ ecrits+=("$f")
117
+ ok_t agents.added opencode "$f"
118
+ elif cmp -s "$f" "$modele"; then
108
119
  ok_t agents.present opencode "$f"
109
120
  elif agents_plugin_repowarden "$f"; then
110
- cp "$MODELE_OPENCODE" "$f"
121
+ cp "$modele" "$f"
111
122
  ecrits+=("$f")
112
123
  ok_t agents.updated opencode "$f"
113
124
  else
114
- attention_t agents.kept opencode "$f" "$MODELE_OPENCODE"
125
+ attention_t agents.kept opencode "$f" "$modele"
126
+ fi
127
+ done
128
+ }
129
+
130
+ # Skill repowarden de l'agent $2 au fichier $1, posée selon la langue :
131
+ # remplacée si posée par repowarden et périmée, gardée si personnalisée
132
+ poser_skill() {
133
+ local f="$1" nom="$2"
134
+ if [ -f "$f" ]; then
135
+ if cmp -s "$f" "$MODELE_SKILL"; then
136
+ ok_t agents.skill_present "$nom" "$f"
137
+ elif agents_skill_repowarden "$f"; then
138
+ cp "$MODELE_SKILL" "$f"
139
+ ecrits+=("$f")
140
+ ok_t agents.skill_updated "$nom" "$f"
141
+ else
142
+ attention_t agents.skill_kept "$nom" "$f" "$MODELE_SKILL"
115
143
  fi
116
144
  return 0
117
145
  fi
118
146
  mkdir -p "$(dirname "$f")"
119
- cp "$MODELE_OPENCODE" "$f"
147
+ cp "$MODELE_SKILL" "$f"
120
148
  ecrits+=("$f")
121
- ok_t agents.added opencode "$f"
149
+ ok_t agents.skill_added "$nom" "$f"
122
150
  }
123
151
 
124
152
  # Hook PreToolUse : seules les commandes « repowarden garde » retirées ; une
@@ -146,16 +174,49 @@ retirer_claude() {
146
174
  ok_t agents.removed "Claude Code" "$f"
147
175
  }
148
176
 
149
- # Plugin opencode : supprimé s'il est celui du modèle ; modifié, gardé et signalé
177
+ # Plugins opencode : chacun supprimé s'il est celui du modèle ; modifié, gardé
178
+ # et signalé
150
179
  retirer_opencode() {
151
- local f="$fichier_opencode"
180
+ local p f
181
+ for p in $AGENTS_PLUGINS_OPENCODE; do
182
+ f="$dossier_opencode/$p"
183
+ if [ ! -f "$f" ]; then
184
+ ok_t agents.absent opencode "$f"
185
+ elif cmp -s "$f" "$AGENTS_MODELES/$p"; then
186
+ rm -f "$f"
187
+ ok_t agents.removed opencode "$f"
188
+ else
189
+ attention_t agents.kept_remove opencode "$f"
190
+ fi
191
+ done
192
+ }
193
+
194
+ # Skill repowarden : retirée si identique au modèle de la langue ; sinon
195
+ # gardée et signalée (peut être modifiée ou dans une autre langue)
196
+ retirer_skill_claude() {
197
+ local f="$fichier_skill_claude" nom="Claude Code"
198
+ if [ ! -f "$f" ]; then
199
+ ok_t agents.skill_absent "$nom" "$f"
200
+ elif cmp -s "$f" "$MODELE_SKILL"; then
201
+ rm -f "$f"
202
+ rmdir "$(dirname "$f")" 2>/dev/null || true
203
+ ok_t agents.skill_removed "$nom" "$f"
204
+ else
205
+ attention_t agents.skill_kept_remove "$nom" "$f"
206
+ fi
207
+ }
208
+
209
+ # Skill repowarden pour opencode : même règle
210
+ retirer_skill_opencode() {
211
+ local f="$fichier_skill_opencode" nom="opencode"
152
212
  if [ ! -f "$f" ]; then
153
- ok_t agents.absent opencode "$f"
154
- elif cmp -s "$f" "$MODELE_OPENCODE"; then
213
+ ok_t agents.skill_absent "$nom" "$f"
214
+ elif cmp -s "$f" "$MODELE_SKILL"; then
155
215
  rm -f "$f"
156
- ok_t agents.removed opencode "$f"
216
+ rmdir "$(dirname "$f")" 2>/dev/null || true
217
+ ok_t agents.skill_removed "$nom" "$f"
157
218
  else
158
- attention_t agents.kept_remove opencode "$f"
219
+ attention_t agents.skill_kept_remove "$nom" "$f"
159
220
  fi
160
221
  }
161
222
 
@@ -163,10 +224,14 @@ statut=0
163
224
  if [ "$action" = retirer ]; then
164
225
  [ -z "$claude" ] || retirer_claude || statut=1
165
226
  [ -z "$opencode" ] || retirer_opencode || statut=1
227
+ [ -z "$claude" ] || retirer_skill_claude || statut=1
228
+ [ -z "$opencode" ] || retirer_skill_opencode || statut=1
166
229
  exit "$statut"
167
230
  fi
168
231
  [ -z "$claude" ] || init_claude || statut=1
169
232
  [ -z "$opencode" ] || init_opencode || statut=1
233
+ [ -z "$claude" ] || poser_skill "$fichier_skill_claude" "Claude Code" || statut=1
234
+ [ -z "$opencode" ] || poser_skill "$fichier_skill_opencode" "opencode" || statut=1
170
235
 
171
236
  has repowarden || attention_t agents.no_repowarden
172
237
  # Hooks repowarden inactifs : le garde-fou refusera les commits, autant le
package/bin/carte ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Usage : repowarden carte (re)génère la carte
3
+ # repowarden carte <fichier|fonction|'#n'> [--json] extrait utile
4
+ #
5
+ # Carte du projet, sans LLM (gh seulement pour la conception des tickets) : chaque fonction des scripts shell,
6
+ # son fichier, sa ligne et son « pourquoi » (le commentaire juste au-dessus),
7
+ # dans .repowarden/carte/carte.json (dossier ignoré par git). Interrogée,
8
+ # elle est d'abord régénérée si elle est absente ou périmée. Voir docs/carte.md.
9
+ set -euo pipefail
10
+
11
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
12
+ # shellcheck source=../hooks/lib/common.sh
13
+ source "$ROOT/hooks/lib/common.sh"
14
+ # shellcheck source=../hooks/lib/lang.sh
15
+ source "$ROOT/hooks/lib/lang.sh"
16
+ # shellcheck source=../hooks/lib/carte.sh
17
+ source "$ROOT/hooks/lib/carte.sh"
18
+
19
+ # Arguments : une cible (fichier ou fonction), --json
20
+ cible="" json=""
21
+ for a in "$@"; do
22
+ case "$a" in
23
+ --json) json=json ;;
24
+ -*) inconnu="$a" ;;
25
+ *) if [ -z "$cible" ]; then cible="$a"; else inconnu="$a"; fi ;;
26
+ esac
27
+ if [ -n "${inconnu:-}" ]; then
28
+ err_t install.unknown_arg "$inconnu"
29
+ _tr help.carte
30
+ printf '%b\n' "$_T" >&2
31
+ exit 1
32
+ fi
33
+ done
34
+ if [ -z "$GIT_TOPLEVEL" ]; then
35
+ err_t carte.no_repo
36
+ exit 1
37
+ fi
38
+ if ! has jq; then
39
+ err_t carte.no_jq
40
+ exit 1
41
+ fi
42
+ # Fichier existant : chemin depuis la racine du dépôt, quel que soit le dossier courant
43
+ chemin=""
44
+ if [ -n "$cible" ] && [ -e "$cible" ]; then
45
+ chemin="$(cd "$(dirname "$cible")" && pwd -P)/${cible##*/}"
46
+ chemin="${chemin#"$(cd "$GIT_TOPLEVEL" && pwd -P)"/}"
47
+ chemin="${chemin%/.}"
48
+ fi
49
+ cd "$GIT_TOPLEVEL"
50
+
51
+ if [ -z "$cible" ] && [ -z "$json" ]; then
52
+ carte_generer
53
+ # shellcheck disable=SC2016 # programme jq, rien à développer
54
+ read -r fichiers fonctions sans < <(cjq -r '[.fichiers[].fonctions[]?] as $f
55
+ | "\(.fichiers | length) \($f | length) \([$f[] | select(.pourquoi == "")] | length)"' "$CARTE_JSON")
56
+ ok_t carte.done "$fichiers" "$fonctions" "$sans" "$CARTE_JSON"
57
+ exit 0
58
+ fi
59
+ # Interrogation : carte absente ou périmée, régénérée d'abord
60
+ carte_a_jour || carte_generer
61
+ if [ -z "$cible" ]; then
62
+ cat "$CARTE_JSON"
63
+ elif [[ "$cible" =~ ^#([0-9]+)$ ]]; then
64
+ carte_ticket "${BASH_REMATCH[1]}" "$json"
65
+ elif [ -n "$chemin" ] || jq -e --arg f "$cible" '.fichiers | has($f)' "$CARTE_JSON" >/dev/null; then
66
+ carte_fichier "${chemin:-$cible}" "$json"
67
+ else
68
+ carte_fonction "$cible" "$json"
69
+ fi
package/bin/garde CHANGED
@@ -43,10 +43,14 @@ args="$(jq -c ".$a // {}" <<<"$entree")"
43
43
  # Refus : raison et commande à lancer, puis code 2. Ligne shell de plusieurs
44
44
  # commandes (LIGNE_MULTIPLE) : refusée en bloc, ce que l'agent doit savoir
45
45
  LIGNE_MULTIPLE=""
46
+ # Fichier existant dont la modification est vérifiée (chemin depuis la racine) :
47
+ # un refus rappelle de lire sa carte avant de le modifier
48
+ FICHIER_CARTE=""
46
49
  refuser() {
47
50
  _tr "$@"
48
51
  printf '✖ repowarden : %b\n' "$_T" >&2
49
52
  [ -z "$LIGNE_MULTIPLE" ] || { _tr garde.ligne_entiere; printf '%b\n' "$_T" >&2; }
53
+ [ -z "$FICHIER_CARTE" ] || { _tr garde.carte "$FICHIER_CARTE" "$FICHIER_CARTE"; printf '%b\n' "$_T" >&2; }
50
54
  exit 2
51
55
  }
52
56
 
@@ -129,16 +133,39 @@ protegee() {
129
133
  [ -n "$REPLY" ] && [ "$REPLY" = "$1" ]
130
134
  }
131
135
 
136
+ # Vrai si le chemin $1 (absolu), dans le dépôt du dossier courant $2 (son
137
+ # dossier existant le plus proche), est ignoré par git : jamais versionné.
138
+ # Un fichier suivi n'est jamais ignoré (git check-ignore sans --no-index)
139
+ fichier_ignore() {
140
+ local reste="${1#"$2"}" prefixe cdup
141
+ reste="${reste#/}"
142
+ [ -n "$reste" ] || return 1
143
+ # Relatif à la racine par --show-prefix/--show-cdup : ni lien symbolique
144
+ # (/tmp → /private/tmp) ni forme Windows (C:/… contre /c/…) à comparer
145
+ prefixe="$(git rev-parse --show-prefix 2>/dev/null)" || return 1
146
+ cdup="$(git rev-parse --show-cdup 2>/dev/null)" || return 1
147
+ git -C "./$cdup" check-ignore -q -- "$prefixe$reste" 2>/dev/null
148
+ }
149
+
132
150
  # Modification du fichier $1 : seulement sur une branche de travail (de ticket,
133
- # si le dépôt suit ses tickets)
151
+ # si le dépôt suit ses tickets) ; un fichier ignoré par git l'est partout
134
152
  verifier_fichier() {
135
153
  dossier_r "$1"
136
- local d="$REPLY"
154
+ # Sous Windows, jq finit le chemin lu par un CR (retiré) et le chemin peut
155
+ # être en forme Windows (D:/…), absolu lui aussi
156
+ local d="$REPLY" f="${1%$'\r'}"
157
+ case "$f" in /* | [A-Za-z]:[/\\]*) ;; *) f="$cwd/$f" ;; esac
137
158
  (
138
159
  cd "$d" 2>/dev/null || exit 0
139
160
  depot_repowarden || exit 0
140
161
  # Fichiers internes de git (.git/…) : hors du travail de l'agent
141
162
  case "$(pwd -P)/" in "$(cd "$(git rev-parse --git-dir)" && pwd -P)"/*) exit 0 ;; esac
163
+ # Fichier ignoré par git : jamais versionné, règle de branche non applicable
164
+ ! fichier_ignore "$f" "$d" || exit 0
165
+ if [ -f "$f" ]; then
166
+ FICHIER_CARTE="$(cd "$(dirname "$f")" && pwd -P)/${f##*/}"
167
+ FICHIER_CARTE="${FICHIER_CARTE#"$(cd "$(git rev-parse --show-toplevel)" && pwd -P)"/}"
168
+ fi
142
169
  branche="$(git symbolic-ref -q --short HEAD 2>/dev/null || true)"
143
170
  [ -n "$branche" ] || refuser garde.detache "$GIT_TOPLEVEL"
144
171
  if protegee "$branche"; then
@@ -256,11 +283,38 @@ ticket_valide() {
256
283
  [ "$(head -n 1 <<<"$lu")" = OPEN ] && grep -qxF "$VALIDE" <<<"$lu"
257
284
  }
258
285
 
286
+ # Vrai si le texte $1 vise une étiquette de décision du mainteneur (validé,
287
+ # statut: …, à décider), même encodée dans une URL (valid%C3%A9)
288
+ etiquette_decision() {
289
+ local t
290
+ t="$(printf '%b' "${1//%/\\x}")"
291
+ [[ "$t" == *"$VALIDE"* || "$t" == *"statut:"* || "$t" == *"$A_DECIDER"* ]]
292
+ }
293
+
294
+ # Vrai si l'écriture gh api (méthode $1, chemin $2, mots $3, corps lu d'un
295
+ # fichier si $4) pose, retire ou remplace les étiquettes de décision d'un
296
+ # ticket : refusée même avec un ticket validé (#496). Dans le doute, refuser
297
+ api_etiquettes_decision() {
298
+ local m="${1:-POST}" re_etiq='/issues/[0-9]+/labels(/|\?|$)' re_ticket='/issues/[0-9]+/?(\?|$)'
299
+ if [[ "/$2" =~ $re_etiq ]]; then
300
+ # PUT remplace l'ensemble, DELETE sans nom le vide : validé compris
301
+ [ "$m" != PUT ] || return 0
302
+ [ "$m" != DELETE ] || [[ "$2" == */labels/?* ]] || return 0
303
+ [ -z "$4" ] || return 0
304
+ etiquette_decision "$2 $3"
305
+ elif [[ "/$2" =~ $re_ticket ]]; then
306
+ # PATCH du ticket : un champ labels remplace l'ensemble
307
+ [ -n "$4" ] || [[ "$3" == *labels* ]]
308
+ else
309
+ return 1
310
+ fi
311
+ }
312
+
259
313
  # Commande gh (mots $@, sans « gh ») lancée depuis $dir, préfixe
260
314
  # REPOWARDEN_TICKET=$ticket_gh : lectures connues permises, écritures du circuit
261
315
  # par repowarden, décisions au mainteneur, tout le reste avec un ticket validé
262
316
  verifier_gh() {
263
- local ligne="gh $*" pos=() depot="" methode="" champs="" approuve="" mutation="" ecriture="" hors="" rc
317
+ local ligne="gh $*" mots="$*" pos=() depot="" methode="" champs="" corps="" approuve="" mutation="" ecriture="" hors="" rc
264
318
  while [ $# -gt 0 ]; do
265
319
  case "$1" in
266
320
  -R | --repo) shift; depot="${1:-}" ;;
@@ -268,7 +322,8 @@ verifier_gh() {
268
322
  -X | --method) shift; methode="${1:-}" ;;
269
323
  -X?*) methode="${1#-X}" ;;
270
324
  --method=*) methode="${1#--method=}" ;;
271
- -f | -F | --field | --raw-field | --input | -f?* | -F?* | --field=* | --raw-field=* | --input=*) champs=1 ;;
325
+ --input | --input=*) champs=1 corps=1 ;;
326
+ -f | -F | --field | --raw-field | -f?* | -F?* | --field=* | --raw-field=*) champs=1 ;;
272
327
  -a | --approve) approuve=1 ;;
273
328
  -h | --help) return 0 ;;
274
329
  -*) ;;
@@ -303,6 +358,7 @@ verifier_gh() {
303
358
  "api "*)
304
359
  [ -n "$ecriture" ] || exit 0
305
360
  [[ "${pos[1]:-}" != *pending_deployments* ]] || refuser garde.gh_mainteneur "$ligne"
361
+ ! api_etiquettes_decision "$methode" "${pos[1]:-}" "$mots" "$corps" || refuser garde.etiquette_mainteneur "$ligne"
306
362
  hors=1
307
363
  ;;
308
364
  # Lectures connues et description de la PR : permises ; tout le
@@ -324,14 +380,93 @@ verifier_gh() {
324
380
  ) || exit $?
325
381
  }
326
382
 
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).
383
+ # repowarden ticket label <n> <label> [--remove] (mots $@ après « repowarden »),
384
+ # lancé depuis $dir : poser ou retirer « validé » ou un statut valide le ticket,
385
+ # décision du mainteneur refusée même avec REPOWARDEN_TICKET (incident #477)
386
+ verifier_ticket_label() {
387
+ local ligne="repowarden $*" mot decision=""
388
+ case "$1" in
389
+ ticket) [ "${2:-}" = label ] || return 0; shift 2 ;;
390
+ *) shift ;;
391
+ esac
392
+ [ $# -ge 2 ] || return 0
393
+ lecture_seule "$@" && return 0
394
+ # Mots après le numéro : une étiquette entre guillemets y est découpée
395
+ # (« statut: en cours » → statut:), dans le doute refuser
396
+ shift
397
+ for mot in "$@"; do
398
+ etiquette_decision "$mot" && decision=1
399
+ done
400
+ # Étiquette de décision en plusieurs mots (« à décider »), découpée par read -a
401
+ etiquette_decision "$*" && decision=1
402
+ [ -n "$decision" ] || return 0
403
+ dossier_r "$dir"
404
+ (
405
+ cd "$REPLY" 2>/dev/null || exit 0
406
+ depot_repowarden || exit 0
407
+ refuser garde.ticket_mainteneur "$ligne"
408
+ ) || exit $?
409
+ }
410
+
411
+ # repowarden ticket nouveau/new (mots $@), depuis $dir : créer un ticket est
412
+ # une décision du mainteneur, sauf agentsTickets ; --help reste permis (#496)
413
+ verifier_ticket_nouveau() {
414
+ local ligne="repowarden $*"
415
+ [ "${1:-}" = ticket ] || return 0
416
+ case "${2:-}" in nouveau | new) ;; *) return 0 ;; esac
417
+ lecture_seule "$@" && return 0
418
+ dossier_r "$dir"
419
+ (
420
+ cd "$REPLY" 2>/dev/null || exit 0
421
+ depot_repowarden || exit 0
422
+ cfg_r agentsTickets false
423
+ [ "$REPLY" != true ] || exit 0
424
+ refuser garde.ticket_nouveau "$ligne"
425
+ ) || exit $?
426
+ }
427
+
428
+ # Découpe $1 en une commande par ligne (REPLY) : « ; », « | », « && », « || » et
429
+ # le « & » séparateur coupent hors guillemets simples et doubles (antislash
430
+ # compris) ; seul le « & » d'une vraie redirection (N>&M, >&2, >&-, &>, &>>, non
431
+ # échappée) reste dans la commande, citée entière. Dans le doute couper : au pire
432
+ # une commande de trop, jamais une masquée (true \>&git push -f lance git).
433
+ # Entre guillemets le retour à la ligne devient une espace : le contenu exact
434
+ # d'une chaîne n'importe pas pour juger la commande.
331
435
  decouper_lignes() {
332
- local s="$1" out="" c i j prec avant nl=$'\n'
436
+ local s="$1" out="" c q="" i j prec avant nl=$'\n'
333
437
  for ((i = 0; i < ${#s}; i++)); do
334
438
  c="${s:i:1}"
439
+ if [ -n "$q" ]; then
440
+ if [ "$c" = "$nl" ]; then
441
+ out="$out "
442
+ continue
443
+ fi
444
+ out="$out$c"
445
+ if [ "$c" = "$q" ]; then
446
+ q=""
447
+ elif [ "$c" = "\\" ] && [ "$q" = '"' ]; then
448
+ i=$((i + 1))
449
+ out="$out${s:i:1}"
450
+ fi
451
+ continue
452
+ fi
453
+ case "$c" in
454
+ "'" | '"')
455
+ q="$c"
456
+ out="$out$c"
457
+ continue
458
+ ;;
459
+ "\\")
460
+ out="$out$c"
461
+ i=$((i + 1))
462
+ out="$out${s:i:1}"
463
+ continue
464
+ ;;
465
+ ";" | "|")
466
+ out="$out$nl"
467
+ continue
468
+ ;;
469
+ esac
335
470
  if [ "$c" != "&" ]; then
336
471
  out="$out$c"
337
472
  continue
@@ -367,28 +502,72 @@ decouper_lignes() {
367
502
  REPLY="$out"
368
503
  }
369
504
 
370
- # Ligne de commande shell : chaque commande (&&, ||, ;, |, retour à la ligne),
371
- # « cd » suivi, variables d'environnement en tête ignorées (sauf
372
- # REPOWARDEN_TICKET, le ticket d'une écriture gh hors du circuit des PR)
505
+ # Découpe $1 en mots (MOTS) en respectant guillemets simples et doubles et
506
+ # antislash, sans sous-processus ; guillemet non fermé : MOTS vide, code 1.
507
+ decouper_mots() {
508
+ local s="$1" mot="" q="" c i n
509
+ MOTS=()
510
+ n=${#s}
511
+ for ((i = 0; i < n; i++)); do
512
+ c="${s:i:1}"
513
+ if [ -n "$q" ]; then
514
+ if [ "$c" = "$q" ]; then
515
+ q=""
516
+ elif [ "$c" = "\\" ] && [ "$q" = '"' ]; then
517
+ case "${s:i+1:1}" in
518
+ '"' | "\\" | '$' | '`') i=$((i + 1)); mot="$mot${s:i:1}" ;;
519
+ *) mot="$mot\\" ;;
520
+ esac
521
+ else
522
+ mot="$mot$c"
523
+ fi
524
+ continue
525
+ fi
526
+ case "$c" in
527
+ "'" | '"') q="$c" ;;
528
+ "\\")
529
+ i=$((i + 1))
530
+ mot="$mot${s:i:1}"
531
+ ;;
532
+ " " | $'\t')
533
+ [ -z "$mot" ] || { MOTS+=("$mot"); mot=""; }
534
+ ;;
535
+ *) mot="$mot$c" ;;
536
+ esac
537
+ done
538
+ if [ -n "$q" ]; then
539
+ MOTS=()
540
+ return 1
541
+ fi
542
+ [ -z "$mot" ] || MOTS+=("$mot")
543
+ }
544
+
545
+ # Ligne de commande shell : chaque commande (« ; », « | », « && », « || » et le
546
+ # « & » séparateur, hors guillemets), « cd » suivi, variables d'environnement en
547
+ # tête ignorées (sauf REPOWARDEN_TICKET, le ticket d'une écriture gh hors du
548
+ # circuit des PR)
373
549
  verifier_shell() {
374
- local commande="$1" segment mots m cmd_cc nl=$'\n' lignes ticket_gh
550
+ local commande="$1" segment m cmd_cc lignes ticket_gh
375
551
  dir="$cwd"
376
- # Une commande par ligne. Découpage hors de <( … ) : bash 3.2 (macOS) y
377
- # lit mal ces motifs (« bad substitution ») et ne vérifierait plus rien
378
- lignes="${commande//&&/$nl}"
379
- lignes="${lignes//||/$nl}"
380
- lignes="${lignes//;/$nl}"
381
- 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"
552
+ # Une commande par ligne, en un seul parcours caractère par caractère qui
553
+ # respecte les guillemets (pas de <( … ) : bash 3.2 le lit mal).
554
+ decouper_lignes "$commande"
385
555
  lignes="$REPLY"
386
556
  # Plusieurs commandes : un refus vaut pour toute la ligne (dit à l'agent)
387
557
  [ "$(grep -c '[^[:space:]]' <<<"$lignes")" -le 1 ] || LIGNE_MULTIPLE=1
388
558
  while IFS= read -r segment; do
389
- # shellcheck disable=SC2162 # les antislashs restent tels quels, comme dans la commande
390
- read -a mots <<<"$segment" || true
391
- set -- ${mots[@]+"${mots[@]}"}
559
+ # Guillemet non fermé : commande illisible, refusée dans un dépôt
560
+ # repowarden (dans le doute, refuser) ; ailleurs, tout est permis
561
+ if ! decouper_mots "$segment"; then
562
+ dossier_r "$dir"
563
+ (
564
+ cd "$REPLY" 2>/dev/null || exit 0
565
+ depot_repowarden || exit 0
566
+ refuser garde.commande_illisible "$segment"
567
+ ) || exit $?
568
+ continue
569
+ fi
570
+ set -- ${MOTS[@]+"${MOTS[@]}"}
392
571
  ticket_gh=""
393
572
  while [ $# -gt 0 ]; do
394
573
  case "$1" in
@@ -398,7 +577,13 @@ verifier_shell() {
398
577
  esac
399
578
  done
400
579
  [ $# -gt 0 ] || continue
580
+ # Commande : dernier élément du chemin (/ ou \), sans .exe/.cmd/.bat
401
581
  m="${1//[\"\']/}"
582
+ m="${m##*/}"
583
+ m="${m##*\\}"
584
+ case "$m" in
585
+ *.exe | *.EXE | *.cmd | *.CMD | *.bat | *.BAT) m="${m%.*}" ;;
586
+ esac
402
587
  case "$m" in
403
588
  cd)
404
589
  case "${2:-}" in
@@ -408,21 +593,28 @@ verifier_shell() {
408
593
  *) dir="$dir/${2//[\"\']/}" ;;
409
594
  esac
410
595
  ;;
411
- git | */git)
596
+ git)
412
597
  shift
413
598
  verifier_git "${@//[\"\']/}"
414
599
  ;;
415
- gh | */gh)
600
+ gh)
416
601
  shift
417
602
  verifier_gh "${@//[\"\']/}"
418
603
  ;;
419
- repowarden | */repowarden)
604
+ repowarden)
605
+ shift
606
+ verifier_ticket_nouveau "${@//[\"\']/}"
607
+ set -- repowarden "$@"
420
608
  case "${2//[\"\']/}" in
421
609
  cc | commit)
422
610
  cmd_cc="repowarden ${2//[\"\']/}"
423
611
  shift 2
424
612
  lecture_seule "${@//[\"\']/}" || verifier_cc "$cmd_cc"
425
613
  ;;
614
+ ticket | ticket-label)
615
+ shift
616
+ verifier_ticket_label "${@//[\"\']/}"
617
+ ;;
426
618
  esac
427
619
  ;;
428
620
  esac
@@ -431,7 +623,8 @@ verifier_shell() {
431
623
 
432
624
  case "$outil" in
433
625
  Edit | Write | MultiEdit | NotebookEdit | edit | write | multiedit)
434
- chemin="$(jq -r '.file_path // .filePath // .notebook_path // empty' <<<"$args" 2>/dev/null)"
626
+ # path : outils edit et write d'opencode 2.x
627
+ chemin="$(jq -r '.file_path // .filePath // .notebook_path // .path // empty' <<<"$args" 2>/dev/null)"
435
628
  [ -z "$chemin" ] || verifier_fichier "$chemin"
436
629
  ;;
437
630
  apply_patch)
@@ -440,7 +633,8 @@ case "$outil" in
440
633
  done < <(jq -r '.patchText // empty' <<<"$args" 2>/dev/null |
441
634
  sed -nE -e 's/^\*\*\* (Add|Update|Delete) File: //p' -e 's/^\*\*\* Move to: //p')
442
635
  ;;
443
- Bash | bash)
636
+ # shell : nom de l'outil bash dans opencode 2.x
637
+ Bash | bash | shell)
444
638
  verifier_shell "$(jq -r '.command // empty' <<<"$args" 2>/dev/null)"
445
639
  ;;
446
640
  esac
package/bin/repowarden CHANGED
@@ -7,6 +7,7 @@
7
7
  # repowarden uninstall [--global] [--purge] les retire (avant npm uninstall -g)
8
8
  # repowarden cc assistant de commit
9
9
  # repowarden code-mort nouveau code mort de la branche
10
+ # repowarden carte carte du projet : fonctions et leur pourquoi
10
11
  # repowarden ticket <n> branche du ticket (créée, liée, si besoin)
11
12
  # repowarden ticket nouveau "<titre>" nouveau ticket
12
13
  # repowarden ticket comment <n> "<texte>" commentaire sur un ticket
@@ -52,6 +53,7 @@ help_key_r() {
52
53
  garde | guard) REPLY=help.garde ;;
53
54
  cc | commit) REPLY=help.cc ;;
54
55
  code-mort | deadcode) REPLY=help.code-mort ;;
56
+ carte | map) REPLY=help.carte ;;
55
57
  ticket) REPLY=help.ticket ;;
56
58
  tickets) REPLY=help.tickets ;;
57
59
  pr) REPLY=help.pr ;;
@@ -102,6 +104,7 @@ case "$cmd" in
102
104
  uninstall) exec bash "$ROOT/install.sh" --uninstall "$@" ;;
103
105
  cc | commit) exec bash "$ROOT/bin/commit" "$@" ;;
104
106
  code-mort | deadcode) exec bash "$ROOT/bin/code-mort" "$@" ;;
107
+ carte | map) exec bash "$ROOT/bin/carte" "$@" ;;
105
108
  tickets) exec bash "$ROOT/bin/tickets" "$@" ;;
106
109
  ticket) exec bash "$ROOT/bin/ticket" "$@" ;;
107
110
  ticket-comment) exec bash "$ROOT/bin/ticket-comment" "$@" ;;