repowarden 0.0.0-stage → 4.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -2
  3. package/bin/code-mort +33 -0
  4. package/bin/commit +412 -0
  5. package/bin/npm-publication +151 -0
  6. package/bin/proteger +283 -0
  7. package/bin/repogarde +20 -0
  8. package/bin/repowarden +63 -0
  9. package/bin/statut +100 -0
  10. package/bin/ticket +129 -0
  11. package/bin/tickets +135 -0
  12. package/hooks/commit-msg +64 -0
  13. package/hooks/lang/actions.sh +9 -0
  14. package/hooks/lang/ansible.sh +18 -0
  15. package/hooks/lang/cpp.sh +22 -0
  16. package/hooks/lang/dart.sh +20 -0
  17. package/hooks/lang/deno.sh +15 -0
  18. package/hooks/lang/docker.sh +26 -0
  19. package/hooks/lang/dotnet.sh +15 -0
  20. package/hooks/lang/elixir.sh +13 -0
  21. package/hooks/lang/go.sh +22 -0
  22. package/hooks/lang/gradle.sh +28 -0
  23. package/hooks/lang/helm.sh +28 -0
  24. package/hooks/lang/kubernetes.sh +29 -0
  25. package/hooks/lang/maven.sh +27 -0
  26. package/hooks/lang/node.sh +75 -0
  27. package/hooks/lang/packer.sh +16 -0
  28. package/hooks/lang/php.sh +20 -0
  29. package/hooks/lang/python.sh +79 -0
  30. package/hooks/lang/ruby.sh +24 -0
  31. package/hooks/lang/rust.sh +17 -0
  32. package/hooks/lang/shell.sh +9 -0
  33. package/hooks/lang/swift.sh +20 -0
  34. package/hooks/lang/tasks.sh +16 -0
  35. package/hooks/lang/terraform.sh +29 -0
  36. package/hooks/lib/common.sh +393 -0
  37. package/hooks/lib/compat.sh +24 -0
  38. package/hooks/lib/deadcode.sh +153 -0
  39. package/hooks/lib/forge.sh +39 -0
  40. package/hooks/lib/i18n/en.sh +393 -0
  41. package/hooks/lib/i18n/fr.sh +393 -0
  42. package/hooks/lib/i18n.sh +81 -0
  43. package/hooks/lib/lang.sh +338 -0
  44. package/hooks/lib/safety.sh +108 -0
  45. package/hooks/lib/tickets.sh +184 -0
  46. package/hooks/lib/ui.sh +40 -0
  47. package/hooks/post-checkout +26 -0
  48. package/hooks/post-merge +47 -0
  49. package/hooks/pre-commit +58 -0
  50. package/hooks/pre-push +91 -0
  51. package/hooks/prepare-commit-msg +48 -0
  52. package/install.sh +245 -0
  53. package/package.json +31 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SimBienvenueHoulBoumi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # repowarden
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/SimBienvenueHoulBoumi/repowarden/actions/workflows/ci.yml/badge.svg)](https://github.com/SimBienvenueHoulBoumi/repowarden/actions/workflows/ci.yml)
4
+ [![e2e](https://github.com/SimBienvenueHoulBoumi/repowarden/actions/workflows/e2e.yml/badge.svg)](https://github.com/SimBienvenueHoulBoumi/repowarden/actions/workflows/e2e.yml)
5
+ [![Release](https://img.shields.io/github/v/release/SimBienvenueHoulBoumi/repowarden)](https://github.com/SimBienvenueHoulBoumi/repowarden/releases)
6
+ [![npm](https://img.shields.io/npm/v/repowarden)](https://www.npmjs.com/package/repowarden)
7
+ [![Documentation](https://img.shields.io/badge/docs-site-indigo)](https://simbienvenuehoulboumi.github.io/repowarden/)
8
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/SimBienvenueHoulBoumi/repowarden/badge)](https://scorecard.dev/viewer/?uri=github.com/SimBienvenueHoulBoumi/repowarden)
9
+ [![Licence MIT](https://img.shields.io/badge/licence-MIT-blue)](LICENSE)
10
+
11
+ **repowarden** garde l'entrée de vos dépôts Git : messages de commit, nommage des branches, secrets, formatage et tests, **quel que soit le langage** — sur les postes (hooks), en CI (GitHub, GitLab) et côté serveur.
12
+
13
+ 📖 **Documentation : https://simbienvenuehoulboumi.github.io/repowarden/** · 🇬🇧 **English: https://simbienvenuehoulboumi.github.io/repowarden/en/**
14
+
15
+ > **v4 : repogarde devient repowarden** (paquet npm `repowarden`) et lefthook n'est plus pris en charge. Les anciens noms restent lus en v4. Marche à suivre : [Passer à la v4](https://simbienvenuehoulboumi.github.io/repowarden/migration-v4/).
16
+
17
+ ## Ce qu'il fait
18
+
19
+ - **Commits** : Conventional Commits imposés, préfixe déduit de la branche ; assistant interactif `git cc` (en-tête, corps, pied).
20
+ - **Branches** : format `<type>/<sujet>`, commande de renommage proposée, branches mergées supprimées automatiquement.
21
+ - **Secrets** : gitleaks avant le commit et en CI.
22
+ - **Formatage** : seul le contenu stagé, avec l'outil du projet ; le travail en cours est préservé.
23
+ - **Tests** : au push et en CI, seulement les projets touchés, sur le commit poussé.
24
+ - **Code mort** : seul le nouveau code mort est signalé, prouvé (bloquant) ou candidat (à vérifier) ; `repowarden code-mort` en local.
25
+ - **Tickets** : chaque PR est reliée à un ticket validé (créé automatiquement s'il manque), suivi de « à valider » à « done » à la release ; `repowarden tickets init`.
26
+ - **Versions et releases** : les commits étant conventionnels, la version suivante (semver) et le changelog se déduisent de l'historique ; un workflow réutilisable en fait des releases automatiques (PR de release validée par la CI puis mergée, tag, release), sans jeton ni intervention.
27
+
28
+ **Plus de 25 technologies** : Java (Maven, Gradle), JavaScript / TypeScript, Python, Go, Rust, PHP, Ruby, .NET, Dart / Flutter, Swift, Elixir, C / C++, Shell, Terraform, Packer, Ansible, Helm, Kubernetes, Docker, GitHub Actions — [détail](https://simbienvenuehoulboumi.github.io/repowarden/technologies/). Linux, macOS, Windows.
29
+
30
+ ## Démarrage
31
+
32
+ ### 1. Sur ton poste, en une minute (tous tes dépôts)
33
+
34
+ ```bash
35
+ npm install -g repowarden # 1. le paquet (yarn global add, pnpm add -g : idem)
36
+ repowarden install --global # 2. active les hooks : demande la langue, installe git cc
37
+ repowarden # 3. vérifie : état du poste et étape suivante
38
+ ```
39
+
40
+ L'étape 2 est indispensable : par sécurité, le paquet n'exécute rien à l'installation (aucun script `postinstall`, que npm 12 bloque d'ailleurs). Ensuite, dans **n'importe quel dépôt**, les hooks s'appliquent dès le prochain commit. Rien n'est ajouté aux projets ni à leur `package.json`.
41
+
42
+ ```bash
43
+ cd mon-projet
44
+ git switch -c feat/panier # nom de branche vérifié
45
+ git add . && git cc # assistant : type, scope, description… puis commit
46
+ git push -u origin feat/panier # tests des projets touchés avant l'envoi
47
+ repowarden code-mort # nouveau code mort de ta branche
48
+ ```
49
+
50
+ | Action | Commande |
51
+ |---|---|
52
+ | État du poste, étape suivante | `repowarden` (ou `repowarden statut`) |
53
+ | Toutes les commandes | `repowarden --help` (`cc`, `code-mort`, `proteger`, `npm-publication`…) |
54
+ | Mettre à jour | `npm update -g repowarden` (les hooks suivent) |
55
+ | Désinstaller | `repowarden uninstall --global`, **puis** `npm uninstall -g repowarden` (dans cet ordre : npm n'exécute rien à la désinstallation, les hooks resteraient branchés sur un dossier supprimé) |
56
+
57
+ Sans Node : `git clone https://github.com/SimBienvenueHoulBoumi/repowarden.git ~/repowarden && ~/repowarden/install.sh --global` (mise à jour : `git -C ~/repowarden pull`).
58
+
59
+ **Tester la prochaine version** (avant sa sortie en production) : `npm install -g repowarden@next`, ou `uses: SimBienvenueHoulBoumi/repowarden@develop` en CI. Retour à la version stable : `npm install -g repowarden@latest`. Les problèmes se signalent en [issue](https://github.com/SimBienvenueHoulBoumi/repowarden/issues).
60
+
61
+ ### 2. Dans un projet d'équipe : les mêmes règles pour tous
62
+
63
+ Chaque personne de l'équipe installe repowarden une fois sur son poste (partie 1). Le projet, lui, ajoute deux fichiers, à copier depuis [`templates/project/`](templates/project) :
64
+
65
+ | Fichier | Rôle |
66
+ |---|---|
67
+ | [`.github/workflows/repowarden.yml`](templates/project/.github/workflows/repowarden.yml) | la CI refait toutes les vérifications sur chaque PR : c'est elle qui fait foi, même si quelqu'un contourne les hooks (`git commit --no-verify`) |
68
+ | [`.repowarden.conf`](templates/project/.repowarden.conf) | réglages communs à toute l'équipe, dont la version minimale de repowarden attendue sur les postes |
69
+
70
+ L'essentiel des deux fichiers :
71
+
72
+ ```yaml
73
+ # .github/workflows/repowarden.yml : vérifications de la PR (dernière version 4.x)
74
+ - uses: SimBienvenueHoulBoumi/repowarden@v4
75
+ with: { strict: true } # un outil manquant fait échouer la CI
76
+ ```
77
+
78
+ ```ini
79
+ # .repowarden.conf : un poste en retard est prévenu, avec la commande de mise à jour
80
+ [repowarden]
81
+ version = 3
82
+ ```
83
+
84
+ Puis rendre la CI obligatoire pour merger : `repowarden proteger` (protection de `main`, check `repowarden` exigé).
85
+
86
+ Exemple complet : [repowarden-demo](https://github.com/SimBienvenueHoulBoumi/repowarden-demo) · Releases automatiques : [documentation](https://simbienvenuehoulboumi.github.io/repowarden/releases/).
87
+
88
+ ### 3. Contribuer à repowarden
89
+
90
+ ```bash
91
+ git clone https://github.com/SimBienvenueHoulBoumi/repowarden.git && cd repowarden
92
+ ./install.sh # repowarden se vérifie lui-même (hooks de ce dépôt)
93
+ bats test/ # tests (bats-core), aussi lancés au push
94
+ ```
95
+
96
+ Les PR visent `develop` ; `main` ne reçoit que les livraisons, qui déclenchent les releases. Voir [CONTRIBUTING.md](CONTRIBUTING.md).
97
+
98
+ **Dans VS Code** : l'extension [repowarden-vscode](https://github.com/SimBienvenueHoulBoumi/repowarden-vscode) guide la rédaction des commits, signale une branche mal nommée et des hooks inactifs. En attendant sa publication sur le Marketplace : `.vsix` dans ses [releases](https://github.com/SimBienvenueHoulBoumi/repowarden-vscode/releases), puis `code --install-extension repowarden-X.Y.Z.vsix`.
99
+
100
+ ## Liens
101
+
102
+ [Démarrage rapide](https://simbienvenuehoulboumi.github.io/repowarden/demarrage/) · [Hooks et règles](https://simbienvenuehoulboumi.github.io/repowarden/hooks/) · [Technologies](https://simbienvenuehoulboumi.github.io/repowarden/technologies/) · [Configuration](https://simbienvenuehoulboumi.github.io/repowarden/configuration/) · [CI](https://simbienvenuehoulboumi.github.io/repowarden/ci/) · [Versions et releases](https://simbienvenuehoulboumi.github.io/repowarden/releases/) · [Validation humaine](https://simbienvenuehoulboumi.github.io/repowarden/validation/) · [Code mort](https://simbienvenuehoulboumi.github.io/repowarden/code-mort/) · [En organisation](https://simbienvenuehoulboumi.github.io/repowarden/industrialisation/) · [Sources internes (Nexus, Artifactory)](https://simbienvenuehoulboumi.github.io/repowarden/industrialisation/#8-sources-internes-reseau-ferme-nexus-artifactory) · [Désinstallation](https://simbienvenuehoulboumi.github.io/repowarden/desinstallation/) · [Sécurité](SECURITY.md) · [Contribuer](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
103
+
104
+ Licence [MIT](LICENSE).
package/bin/code-mort ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Code mort introduit par ta branche, comme en CI : seules les lignes ajoutées ou
3
+ # modifiées depuis la base comptent (commits, modifications en cours, nouveaux fichiers).
4
+ # bin/code-mort [base] défaut : point de départ de la branche depuis origin/main
5
+ # Voir docs/code-mort.md : ce qui est prouvé (bloquant) et ce qui est candidat.
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
9
+ # shellcheck source=../hooks/lib/common.sh
10
+ source "$ROOT/hooks/lib/common.sh"
11
+ # shellcheck source=../hooks/lib/lang.sh
12
+ source "$ROOT/hooks/lib/lang.sh"
13
+ cd "$GIT_TOPLEVEL"
14
+
15
+ base="${1:-}"
16
+ label="$base"
17
+ if [ -z "$base" ]; then
18
+ default="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || echo origin/main)"
19
+ label="$default"
20
+ base="$(git merge-base HEAD "$default" 2>/dev/null || git rev-list --max-parents=0 HEAD | tail -n1)"
21
+ fi
22
+ t dc.section
23
+ printf "\n%s━━ %s ━━%s\n" "$UI_B" "$REPLY" "$UI_N"
24
+ changed="$({
25
+ git diff --name-only --diff-filter=ACMR "$base"
26
+ git ls-files --others --exclude-standard
27
+ } | sort -u | without_excluded)"
28
+ # Rien de modifié (ex. sur main à jour) : le dire, plutôt que « aucun langage »
29
+ if [ -z "$changed" ]; then
30
+ info_t dc.no_changes "$label"
31
+ exit 0
32
+ fi
33
+ deadcode_projects "$base" <<<"$changed"
package/bin/commit ADDED
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env bash
2
+ # Assistant de commit conventionnel : guide l'en-tête, le corps et le pied.
3
+ # git cc [options de git commit] alias installé par install.sh
4
+ # bin/commit --dry-run affiche le message sans commiter
5
+ # git cc --strict une réponse refusée arrête tout (scripts :
6
+ # sans cela, la ligne suivante serait lue
7
+ # comme une nouvelle réponse)
8
+ # git cc -m "description" [--type T] [--scope S] [--breaking NOTE]
9
+ # [--body TEXTE] [--refs "Closes #12"]
10
+ # sans question (scripts, agents) : mêmes
11
+ # contrôles, toute réponse refusée arrête
12
+ # tout ; type et scope déduits de la
13
+ # branche s'ils sont omis
14
+ # Les réponses sont lues sur l'entrée standard (terminal avec édition de
15
+ # ligne, ou fichier/tube pour les tests). Le commit passe ensuite par les
16
+ # hooks habituels. Couleurs désactivables : NO_COLOR=1.
17
+ set -euo pipefail
18
+
19
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
20
+ # shellcheck source=../hooks/lib/common.sh
21
+ source "$ROOT/hooks/lib/common.sh"
22
+ # shellcheck source=../hooks/lib/tickets.sh
23
+ source "$ROOT/hooks/lib/tickets.sh"
24
+
25
+ # Charte visuelle commune (hooks/lib/ui.sh, chargée par common.sh)
26
+ B="$UI_B" D="$UI_D" G="$UI_G" R="$UI_R" C="$UI_C" N="$UI_N"
27
+ STEPS=5
28
+
29
+ # Sans question (BATCH) : ni étapes ni aides, seulement les erreurs et l'aperçu
30
+ etape() { [ -z "$BATCH" ] || return 0; printf '\n%s◇ %s/%s%s %s%s%s\n' "$C" "$1" "$STEPS" "$N" "$B" "$2" "$N" >&2; }
31
+ hint() { [ -z "$BATCH" ] || return 0; printf ' %s%s%s\n' "$D" "$*" "$N" >&2; }
32
+ fail() { printf ' %s✖ %s%s\n' "$R" "$*" "$N" >&2; }
33
+ # Réponse refusée : la question est reposée ; avec --strict, abandon (rien
34
+ # n'est commité) plutôt que de lire la réponse suivante d'un script
35
+ refuse() {
36
+ fail "$1"
37
+ [ -z "$STRICT" ] || { t cc.strict_abort; fail "$REPLY"; exit 1; }
38
+ }
39
+
40
+ # Réponse dans REPLY, nettoyée : séquences d'échappement (flèches, Échap) et
41
+ # caractères de contrôle retirés, espaces de début et de fin supprimés.
42
+ # Dans un terminal, édition de ligne (flèches, effacement) avec read -e.
43
+ # Sans question (BATCH) : la réponse est ANSWER, fixée par l'appelant.
44
+ ask() {
45
+ if [ -n "$BATCH" ]; then
46
+ REPLY="$ANSWER"
47
+ elif [ -t 0 ]; then
48
+ # Ctrl+D (fin de saisie) : abandon
49
+ IFS= read -e -r -p "$1" REPLY || { echo >&2; t cc.aborted; fail "$REPLY"; exit 1; }
50
+ else
51
+ printf '%s' "$1" >&2
52
+ # Fin de la saisie (entrée épuisée) : abandon, jamais de boucle infinie
53
+ if ! IFS= read -r REPLY; then
54
+ [ -n "$REPLY" ] || { echo >&2; t cc.aborted; fail "$REPLY"; exit 1; }
55
+ fi
56
+ fi
57
+ REPLY="$(printf '%s' "$REPLY" | LC_ALL=C sed -e $'s/\x1b\\[[0-9;?]*[ -\\/]*[@-~]//g' -e $'s/\x1b.//g' |
58
+ LC_ALL=C tr -d '\000-\010\013-\037\177')"
59
+ REPLY="${REPLY#"${REPLY%%[![:space:]]*}"}"
60
+ REPLY="${REPLY%"${REPLY##*[![:space:]]}"}"
61
+ }
62
+
63
+ # Question oui/non : $1 invite, $2 réponse par défaut (o ou n). Toute autre
64
+ # réponse est refusée et la question reposée. Résultat : YN=1 pour oui.
65
+ ask_yn() {
66
+ while :; do
67
+ ask "$1"
68
+ case "${REPLY:-$2}" in
69
+ o | O | oui | Oui | OUI | y | Y | yes | Yes | YES) YN=1; return 0 ;;
70
+ n | N | non | Non | NON | no | No | NO) YN=""; return 0 ;;
71
+ esac
72
+ t cc.yn_invalid
73
+ refuse "$REPLY"
74
+ done
75
+ }
76
+
77
+ DRY_RUN="" STRICT="" BATCH="" ANSWER=""
78
+ PASS=() # options transmises à git commit (--no-verify…)
79
+ OPT_TYPE="" OPT_SCOPE="" OPT_MESSAGE="" OPT_BODY="" OPT_REFS="" OPT_BREAKING=""
80
+ # Valeur d'une option : « --type feat » ou « --type=feat »
81
+ option_value() {
82
+ case "$1" in
83
+ *=*) REPLY="${1#*=}"; SHIFT=1 ;;
84
+ *)
85
+ [ $# -ge 2 ] || { t cc.option_value "$1"; fail "$REPLY"; exit 1; }
86
+ REPLY="$2"; SHIFT=2
87
+ ;;
88
+ esac
89
+ }
90
+ while [ $# -gt 0 ]; do
91
+ SHIFT=1
92
+ case "$1" in
93
+ --dry-run) DRY_RUN=1 ;;
94
+ --strict) STRICT=1 ;;
95
+ --type | --type=*) option_value "$@"; OPT_TYPE="$REPLY"; BATCH=1 ;;
96
+ --scope | --scope=*) option_value "$@"; OPT_SCOPE="$REPLY"; BATCH=1 ;;
97
+ -m | --message | --message=*) option_value "$@"; OPT_MESSAGE="$REPLY"; BATCH=1 ;;
98
+ --body | --body=*) option_value "$@"; OPT_BODY="$REPLY"; BATCH=1 ;;
99
+ --refs | --refs=*) option_value "$@"; OPT_REFS="$REPLY"; BATCH=1 ;;
100
+ --breaking | --breaking=*) option_value "$@"; OPT_BREAKING="$REPLY"; BATCH=1 ;;
101
+ --) shift; PASS+=("$@"); break ;;
102
+ *) PASS+=("$1") ;;
103
+ esac
104
+ shift "$SHIFT"
105
+ done
106
+ # Sans question : les valeurs viennent des options, aucune n'est redemandée
107
+ if [ -n "$BATCH" ]; then
108
+ STRICT=1
109
+ [ -n "$OPT_MESSAGE" ] || { t cc.batch_message_required; fail "$REPLY"; exit 1; }
110
+ fi
111
+
112
+ if [ -z "$DRY_RUN" ] && git diff --cached --quiet; then
113
+ t cc.nothing_staged
114
+ fail "$REPLY"
115
+ exit 1
116
+ fi
117
+
118
+ TYPES=(feat fix docs style refactor perf test build ci chore revert)
119
+ # Emojis à la gitmoji : repères visuels du menu, jamais écrits dans le message
120
+ EMOJIS=(✨ 🐛 📝 🎨 ♻️ ⚡ ✅ 📦 👷 🔧 ⏪)
121
+ DESCRIPTIONS=()
122
+ for _type in "${TYPES[@]}"; do
123
+ t "cc.type.$_type"
124
+ DESCRIPTIONS+=("$REPLY")
125
+ done
126
+
127
+ # Valeurs proposées d'après la branche : feat/panier → feat, panier. Branche
128
+ # hors convention (wip/xyz, essai/xyz) : rien n'est proposé, le type doit être
129
+ # choisi (Entrée donnerait sinon un feat, donc une version mineure, par erreur).
130
+ branch="$(git symbolic-ref --short HEAD 2>/dev/null || echo "")"
131
+ first_commit=""
132
+ git rev-parse -q --verify HEAD >/dev/null || first_commit=1
133
+ default_type="feat"
134
+ default_scope=""
135
+ if [[ "$branch" == */* ]]; then
136
+ prefix="${branch%%/*}"
137
+ case "$prefix" in feature) prefix=feat ;; bugfix | hotfix) prefix=fix ;; esac
138
+ if [[ "$prefix" =~ ^($CC_TYPES)$ ]]; then
139
+ default_type="$prefix"
140
+ default_scope="$(echo "${branch#*/}" | tr '[:upper:]/' '[:lower:]-' | tr -cd 'a-z0-9._-')"
141
+ [ "${#default_scope}" -le 20 ] || default_scope=""
142
+ else
143
+ default_type=""
144
+ fi
145
+ fi
146
+
147
+ if [ -z "$BATCH" ]; then
148
+ t cc.title
149
+ brand "$REPLY" >&2
150
+ t cc.help
151
+ printf '%s%s%s\n' "$D" "$REPLY" "$N" >&2
152
+ fi
153
+
154
+ # 1. Type
155
+ t cc.step1
156
+ etape 1 "$REPLY"
157
+ [ -n "$BATCH" ] || for i in "${!TYPES[@]}"; do
158
+ printf ' %s%2d%s %s %s%-9s%s %s%s%s\n' "$D" "$((i + 1))" "$N" "${EMOJIS[$i]}" "$B" "${TYPES[$i]}" "$N" "$D" "${DESCRIPTIONS[$i]}" "$N" >&2
159
+ done
160
+ while :; do
161
+ if [ -n "$default_type" ]; then t cc.type_prompt "$default_type"; else t cc.type_prompt_required; fi
162
+ ANSWER="$OPT_TYPE"
163
+ ask "$REPLY"
164
+ answer="${REPLY:-$default_type}"
165
+ if [ -z "$answer" ]; then
166
+ t cc.type_required
167
+ refuse "$REPLY"
168
+ continue
169
+ elif [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "${#TYPES[@]}" ]; then
170
+ type="${TYPES[$((answer - 1))]}"
171
+ break
172
+ elif [[ "$answer" =~ ^($CC_TYPES)$ ]]; then
173
+ type="$answer"
174
+ break
175
+ fi
176
+ t cc.type_invalid "$answer" "${#TYPES[@]}"
177
+ refuse "$REPLY"
178
+ done
179
+
180
+ # 2. Scope (optionnel)
181
+ t cc.step2
182
+ etape 2 "$REPLY"
183
+ t cc.scope_hint
184
+ hint "$REPLY"
185
+ while :; do
186
+ if [ -n "$default_scope" ]; then
187
+ t cc.scope_prompt_default "$default_scope"
188
+ ANSWER="$OPT_SCOPE"
189
+ ask "$REPLY"
190
+ scope="${REPLY:-$default_scope}"
191
+ [ "$scope" = - ] && scope=""
192
+ else
193
+ t cc.scope_prompt
194
+ ANSWER="$OPT_SCOPE"
195
+ ask "$REPLY"
196
+ scope="$REPLY"
197
+ [ "$scope" = - ] && scope="" # « - » : aucun scope, comme avec une proposition
198
+ fi
199
+ [[ -z "$scope" || "$scope" =~ ^[a-z0-9._-]+$ ]] && break
200
+ # Version corrigée proposée (« Doc Test » → « doc-test ») : Entrée pour l'accepter
201
+ fixed="$(printf '%s' "$scope" | tr '[:upper:] ' '[:lower:]-' | LC_ALL=C tr -cd 'a-z0-9._-' | tr -s '-')"
202
+ fixed="${fixed#-}"
203
+ fixed="${fixed%-}"
204
+ if [ -n "$fixed" ]; then
205
+ t cc.scope_fixed "$fixed"
206
+ default_scope="$fixed"
207
+ else
208
+ t cc.scope_invalid
209
+ fi
210
+ refuse "$REPLY"
211
+ done
212
+
213
+ # Branche protégée (main…) : le commit y serait refusé. On le dit tout de suite
214
+ # et on propose une branche <type>/<scope>, les modifications suivent.
215
+ cfg_r protectedBranches "main master"
216
+ protected="$REPLY" # à garder : skipped réécrit REPLY
217
+ if [ -z "$first_commit" ] && [ -z "$DRY_RUN" ] && ! skipped protect-branch &&
218
+ [[ " $protected " == *" $branch "* ]]; then
219
+ t cc.topic_default
220
+ suggestion="$type/${scope:-$REPLY}"
221
+ t cc.protected_hint "$branch"
222
+ printf '\n %s%s%s\n' "$B" "$REPLY" "$N" >&2
223
+ while :; do
224
+ t cc.new_branch_prompt "$suggestion"
225
+ ANSWER=n # sans question : jamais de branche créée à la place de l'utilisateur
226
+ ask "$REPLY"
227
+ name="${REPLY:-$suggestion}"
228
+ # Réponse incomplète ou approximative : complétée (« feat » → feat/<scope>,
229
+ # « Mon Test » → feat/mon-test) puis proposée, sans refus sec
230
+ if [[ "$name" != */* && "$name" =~ ^($CC_TYPES|feature|bugfix|hotfix)$ ]]; then
231
+ name="$name/${suggestion#*/}"
232
+ fi
233
+ case "$name" in n | N | non | no)
234
+ t cc.aborted
235
+ printf '%s%s%s\n' "$D" "$REPLY" "$N" >&2
236
+ exit 1
237
+ ;;
238
+ esac
239
+ if branch_name_valid "$name" && ! git show-ref -q --verify "refs/heads/$name" &&
240
+ git switch -q -c "$name" </dev/null >/dev/null 2>&1; then
241
+ branch="$name"
242
+ t cc.branch_created "$name"
243
+ hint "$REPLY"
244
+ break
245
+ fi
246
+ if git show-ref -q --verify "refs/heads/$name"; then
247
+ t cc.branch_exists "$name"
248
+ else
249
+ suggestion="$(suggest_branch_name "$name")"
250
+ t cc.branch_fixed "$name" "$suggestion"
251
+ fi
252
+ refuse "$REPLY"
253
+ done
254
+ fi
255
+
256
+ # 3. Changement incompatible (version majeure) : sans objet au premier commit
257
+ t cc.step3
258
+ etape 3 "$REPLY"
259
+ breaking=""
260
+ breaking_note=""
261
+ if [ -n "$first_commit" ]; then
262
+ t cc.first_commit
263
+ hint "$REPLY"
264
+ else
265
+ t cc.breaking_hint1
266
+ hint "$REPLY"
267
+ t cc.breaking_hint2
268
+ hint "$REPLY"
269
+ t cc.breaking_prompt
270
+ ANSWER="${OPT_BREAKING:+o}"
271
+ ask_yn "$REPLY" n
272
+ breaking="$YN"
273
+ if [ -n "$breaking" ]; then
274
+ t cc.breaking_note_hint
275
+ hint "$REPLY"
276
+ while :; do
277
+ t cc.breaking_note_prompt
278
+ ANSWER="$OPT_BREAKING"
279
+ ask "$REPLY"
280
+ if [ "${#REPLY}" -ge 10 ] && [[ ! "$REPLY" =~ ^(RAS|ras|rien|aucun|none|-|\.)$ ]]; then
281
+ breaking_note="$REPLY"
282
+ break
283
+ fi
284
+ t cc.breaking_note_invalid
285
+ refuse "$REPLY"
286
+ done
287
+ fi
288
+ fi
289
+
290
+ header_prefix="$type${scope:+($scope)}${breaking:+!}: "
291
+
292
+ # 4. Description, avec contrôle de longueur de l'en-tête
293
+ t cc.step4
294
+ etape 4 "$REPLY"
295
+ authored_length "$header_prefix"
296
+ max=$((72 - REPLY))
297
+ t cc.desc_hint "$max"
298
+ hint "$REPLY"
299
+ while :; do
300
+ ANSWER="$OPT_MESSAGE"
301
+ ask "$header_prefix"
302
+ description="$REPLY"
303
+ description="${description%.}"
304
+ if [ -z "$description" ]; then
305
+ t cc.desc_required
306
+ refuse "$REPLY"
307
+ continue
308
+ fi
309
+ authored_length "$header_prefix$description"
310
+ if [ "$REPLY" -gt 72 ]; then
311
+ t cc.header_too_long "$REPLY"
312
+ refuse "$REPLY"
313
+ continue
314
+ fi
315
+ break
316
+ done
317
+
318
+ # 5. Détails (optionnels) : corps puis références
319
+ t cc.step5
320
+ etape 5 "$REPLY"
321
+ t cc.body_hint
322
+ hint "$REPLY"
323
+ body=""
324
+ if [ -n "$BATCH" ]; then
325
+ [ -z "$OPT_BODY" ] || body="${OPT_BODY%$'\n'}"$'\n'
326
+ fi
327
+ while [ -z "$BATCH" ]; do
328
+ ask "> "
329
+ # Ligne vide, ou faite seulement de ponctuation (« ; », « . »…) : fin du corps
330
+ [[ -n "$REPLY" && ! "$REPLY" =~ ^[[:punct:]]+$ ]] || break
331
+ body="$body$REPLY"$'\n'
332
+ done
333
+ # Branche type/12-sujet : ticket #12 proposé (Entrée l'accepte, « - » : aucun)
334
+ ticket=""
335
+ branch_ticket_r "$branch"
336
+ [ -z "$REPLY" ] || ticket="Ticket : #$REPLY"
337
+ # Branche de travail sans ticket : prévenu, jamais bloqué. Création proposée
338
+ # seulement si repowarden gère la vie des tickets du projet (et sans -m)
339
+ if [ -z "$ticket" ] && [ -z "$DRY_RUN" ] && ! skipped tickets && work_branch "$branch"; then
340
+ if [ -z "$BATCH" ] && tickets_managed && has gh; then
341
+ t cc.ticket_offer
342
+ ask_yn "$REPLY" n
343
+ if [ -n "$YN" ] && n="$(bash "$ROOT/bin/ticket" nouveau "$description" --type "$type" --porcelain)" && [ -n "$n" ]; then
344
+ ticket="Ticket : #$n"
345
+ t cc.ticket_created "$n"
346
+ hint "$REPLY"
347
+ fi
348
+ else
349
+ ticket_hint "$branch"
350
+ fi
351
+ fi
352
+ if [ -n "$ticket" ]; then
353
+ t cc.refs_prompt_ticket "$ticket"
354
+ else
355
+ t cc.refs_prompt "$FORGE_REFS"
356
+ fi
357
+ ANSWER="$OPT_REFS"
358
+ ask "$REPLY"
359
+ refs="${REPLY:-$ticket}"
360
+ [ "$refs" != - ] || refs=""
361
+
362
+ # Message final : en-tête, puis corps et pied séparés par une ligne vide
363
+ footer=""
364
+ [ -z "$breaking_note" ] || footer="BREAKING CHANGE: $breaking_note"$'\n'
365
+ [ -z "$refs" ] || footer="$footer$refs"$'\n'
366
+ message="$header_prefix$description"$'\n'
367
+ [ -z "$body" ] || message="$message"$'\n'"$body"
368
+ [ -z "$footer" ] || message="$message"$'\n'"$footer"
369
+
370
+ t cc.preview
371
+ printf '\n%s┌─ %s ───────────────────────────────────────────────%s\n' "$D" "$REPLY" "$N" >&2
372
+ while IFS= read -r line; do
373
+ printf '%s│%s %s\n' "$D" "$N" "$line" >&2
374
+ done <<<"${message%$'\n'}"
375
+ printf '%s└─────────────────────────────────────────────────────────%s\n' "$D" "$N" >&2
376
+
377
+ if [ -n "$DRY_RUN" ]; then
378
+ printf '%s' "$message"
379
+ exit 0
380
+ fi
381
+
382
+ t cc.confirm
383
+ ANSWER=o
384
+ ask_yn "$REPLY" o
385
+ if [ -z "$YN" ]; then
386
+ t cc.aborted
387
+ printf '%s%s%s\n' "$D" "$REPLY" "$N" >&2
388
+ exit 1
389
+ fi
390
+
391
+ # Message gardé dans le dépôt si le commit est refusé (hook, secret…) : rien à ressaisir
392
+ file="$(git rev-parse --git-dir)/repowarden-message"
393
+ printf '%s' "$message" >"$file"
394
+ if ! git commit -F "$file" ${PASS[@]+"${PASS[@]}"}; then
395
+ t cc.kept "$file"
396
+ printf '\n%s%s%s\n' "$B" "$REPLY" "$N" >&2
397
+ exit 1
398
+ fi
399
+ rm -f "$file"
400
+
401
+ # Suite : rappel de la convention de branches
402
+ cfg_r protectedBranches "main master"
403
+ protected="$REPLY"
404
+ t cc.created
405
+ created="$REPLY"
406
+ if [[ " $protected " == *" $branch "* ]]; then
407
+ t cc.topic
408
+ t cc.next_branch "${B}git switch -c $type/<$REPLY>$N" "$FORGE_PR"
409
+ else
410
+ t cc.next_push "${B}git push -u origin $branch$N"
411
+ fi
412
+ printf '\n%s%s✔ %s%s %s\n' "$G" "$B" "$created" "$N" "$REPLY" >&2