diff --git a/A_FAIRE.md b/A_FAIRE.md
deleted file mode 100644
index 02c71b8..0000000
--- a/A_FAIRE.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# État du paquet
-
-Mis à jour le 2026-08-18, après une installation, une sauvegarde et une mise à jour
-réussies sur le serveur réel.
-
-**Aucune tâche bloquante.** Ce fichier garde les contraintes permanentes et le
-journal de ce qui a été réglé — le *pourquoi* resservira à la prochaine version.
-
----
-
-## Publier une nouvelle version
-
-📖 La marche à suivre est dans [`PUBLICATION.md`](PUBLICATION.md) — première mise en
-production, montée de version, retour arrière, désinstallation.
-
-En résumé : `./build/publier-release.sh --version X.Y.Z`, tag et release sur le dépôt
-du code, téléversement de l'archive, puis commit **et push** du manifeste.
-
-Le suffixe `~ynhN` se bump à la main quand seul le paquet change (conf, scripts) sans
-nouvelle archive — c'est ce qui a été fait pour `~ynh2` et `~ynh3`.
-
----
-
-# Contraintes permanentes — ce ne sont pas des tâches
-
-## Le dépôt du code doit rester public
-
-`ynh_setup_source` télécharge **sans jeton**. Sur un dépôt privé, Gitea répond
-**404 et non 403** à un anonyme : le symptôme est identique à « la release n'existe
-pas », ce qui envoie chercher au mauvais endroit. Ne pas mettre de jeton dans l'URL
-du manifeste — il serait lisible sur le serveur.
-
-## L'application exige un domaine entier, pas un sous-chemin
-
-MaBibli s'installe sur `mabibli.mondomaine.tld`, **pas** sur `mondomaine.tld/mabibli`.
-
-Deux éléments sont figés **à la compilation** du client Blazor WebAssembly : la balise
-`` de `index.html`, et les empreintes d'intégrité de
-`service-worker-assets.js`. Les réécrire sur le serveur casserait le service worker,
-donc le mode hors-ligne — et rien n'est recompilé sur le serveur, c'est tout l'intérêt
-du self-contained. Le paquet déclare donc l'application en `full_domain`.
-
-Lever cette contrainte demanderait une archive **par chemin d'installation**, ou une
-compilation sur le serveur. Les deux annulent le bénéfice du self-contained.
-
-## `install_dir` finit par appartenir à l'application, pas à root
-
-Le manifeste demande `owner = "root:rwx"` pour que le service ne puisse pas réécrire
-ses binaires, mais le helper `_ynh_apply_default_permissions` repasse derrière avec un
-`chown -R mabibli:mabibli`. L'intention tient quand même, portée par
-`ProtectSystem=strict` dans l'unité systemd : tout est en lecture seule sauf
-`ReadWritePaths=`, qui ne liste que le `data_dir`.
-
-⚠️ Ne pas retirer `ProtectSystem=strict` en croyant que la propriété des fichiers
-protège encore.
-
----
-
-# Fait
-
-## Les deux pièges systemd — 2026-08-18
-
-Découverts à la première installation réelle, chacun a coûté un cycle complet. Ni
-l'un ni l'autre ne peut sortir d'un lancement du binaire à la main : ils tiennent au
-gestionnaire de services.
-
-| Piège | Symptôme | Correction |
-|---|---|---|
-| `Environment=` découpe sur les espaces | `ArgumentException … at index 0` | guillemeter toute la ligne |
-| `ProtectHome=yes` masque `/home` | `SQLite Error 14: unable to open database file` | `ProtectHome=tmpfs` + `BindPaths=` |
-
-Détail et mesures dans les commentaires de `conf/systemd.service`, à ne pas retirer.
-
-## L'URL de démonstration a été remplacée — 2026-08-18
-
-`manifest.toml` portait `gitea.example.org` : l'installation s'arrêtait net au
-`Prefetching asset main`. Corrigé partout (`manifest.toml`, `README.md`,
-`conf/systemd.service`, et la valeur par défaut de `--base-url`). Contrôle :
-`grep -rn "gitea.example.org" .` ne doit plus rien remonter que ce fichier-ci.
-
-## L'archive ne va plus dans git — 2026-08-18
-
-`build/dist/` est ignoré. L'archive vit dans la release Gitea ; committée, elle
-ajouterait 69 Mo d'historique **par version**, définitivement.
diff --git a/PUBLICATION.md b/PUBLICATION.md
deleted file mode 100644
index 2ee499a..0000000
--- a/PUBLICATION.md
+++ /dev/null
@@ -1,262 +0,0 @@
-# Mettre MaBibli en production, et la mettre à jour
-
-Deux marches à suivre, éprouvées sur un serveur réel le 2026-08-18 : la **première mise
-en production**, puis la **montée de version**. Les contraintes qui les gouvernent sont
-expliquées dans `A_FAIRE.md` — ici, ce sont les gestes.
-
-Principe qui explique toute la chaîne : **le serveur ne compile jamais**. Il télécharge
-une archive déjà produite, vérifie son empreinte, et la déploie. Le SDK .NET n'est
-nécessaire que sur la machine de développement.
-
-```
-machine de dev Gitea serveur YunoHost
-────────────── ───── ────────────────
-publier-release.sh
- → archive .tar.gz ────────► release du dépôt `mabibli`
- → manifest.toml ────────► dépôt `mabibli_ynh` ──────► yunohost app install
- (url + sha256) (vérifie le sha256)
-```
-
----
-
-# A. Première mise en production
-
-## A.1 Préalables, une seule fois
-
-- Le dépôt du code (`mabibli`) est **public** sur Gitea. `ynh_setup_source` télécharge
- sans jeton ; sur un dépôt privé Gitea répond 404 — indiscernable d'une release
- absente.
-- Le domaine dédié existe côté YunoHost. MaBibli s'installe sur un **domaine entier**
- (`mabibli.mondomaine.tld`), jamais sur un sous-chemin :
-
-```bash
-sudo yunohost domain add mabibli.mondomaine.tld
-```
-
-- Le SDK .NET est installé sur la machine de développement (`dotnet --version`).
-
-## A.2 Produire l'archive
-
-Depuis `mabibli_ynh`, avec le dépôt du code à côté (`../mabibli`) :
-
-```bash
-./build/publier-release.sh
-```
-
-Le script compile en `Release` self-contained, **vérifie le publish** (binaire présent,
-`wwwroot/` embarqué, toutes les ressources d'`index.html` réellement sur disque, aucun
-placeholder d'empreinte non substitué), produit l'archive de façon reproductible, en
-calcule le `sha256`, et réécrit `version`, `amd64.url` et `amd64.sha256` dans
-`manifest.toml`.
-
-À contrôler dans sa sortie : la ligne `binaire, wwwroot et ressources d'index.html : OK`,
-et le `sha256` affiché — c'est celui que le manifeste porte désormais.
-
-## A.3 Déposer la release
-
-```bash
-cd ../mabibli && git tag v0.1.0 && git push origin main --tags
-```
-
-Puis, **dans l'interface Gitea** : créer la release `v0.1.0` sur le dépôt `mabibli`, et y
-téléverser `build/dist/mabibli-0.1.0-linux-x64.tar.gz`.
-
-Vérification, à faire **sans être authentifié** (autre navigateur, ou `curl` comme
-ci-dessous) :
-
-```bash
-curl -fsSLI "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v0.1.0/mabibli-0.1.0-linux-x64.tar.gz" | head -1
-```
-
-Attendu : `HTTP/2 200`. Un 404 signifie soit que la release n'est pas déposée, soit que
-le dépôt est privé — les deux se ressemblent, commencer par vérifier la visibilité.
-
-## A.4 Pousser le paquet
-
-⚠️ **L'étape la plus facile à oublier.** `yunohost app install ` lit le manifeste
-**depuis Gitea**, jamais la copie locale : un manifeste corrigé mais non poussé n'existe
-pas pour le serveur.
-
-```bash
-cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.0" && git push
-```
-
-## A.5 Installer
-
-```bash
-sudo yunohost app install https://git.akbar.nohost.me/mathieu/mabibli_ynh --debug
-```
-
-YunoHost demande le domaine (celui créé en A.1) et le groupe autorisé (`all_users`).
-
-**Ce qu'il faut voir passer :** `Prefetching asset main` sur la bonne URL, puis
-l'installation des fichiers, nginx, systemd, et enfin `The service mabibli has correctly
-executed the action start` — le script attend la ligne `Application started` du journal,
-ce qui fait échouer franchement l'installation si les migrations EF Core ne passent pas.
-
-## A.6 Vérifier
-
-```bash
-sudo ss -tlnp | grep -i mabibli
-```
-
-Doit montrer **`127.0.0.1:` uniquement**. C'est une frontière de sécurité : sur
-`0.0.0.0`, n'importe qui sur le réseau pourrait forger l'en-tête `YNH_USER` et se faire
-passer pour un membre du foyer.
-
-```bash
-sudo ls -l /home/yunohost.app/mabibli
-```
-
-`mabibli.db` doit exister : les migrations se sont appliquées seules au premier
-démarrage. (Pas de `-wal` ni `-shm` au repos, c'est normal — SQLite fait un checkpoint à
-la fermeture de la dernière connexion.)
-
-Enfin, dans un navigateur sur `https://mabibli.mondomaine.tld` : le portail authentifie,
-et l'application affiche **ton** nom d'utilisateur YunoHost — pas « anonyme ». C'est le
-seul contrôle qui éprouve réellement l'intégration SSO.
-
----
-
-# B. Monter de version — exemple : 0.1.0 → 0.1.1
-
-## B.1 Ce qui distingue une version applicative d'une révision de paquet
-
-| Ce qui change | Version | Nouvelle archive ? |
-|---|---|---|
-| Le code C# | `0.1.0` → `0.1.1~ynh1` | **oui** |
-| Seulement le paquet (conf systemd/nginx, scripts) | `0.1.0~ynh1` → `0.1.0~ynh2` | non |
-
-Le second cas est le plus simple : bump du suffixe `~ynhN` **à la main** dans
-`manifest.toml`, commit, push, puis directement l'étape B.5. L'archive et son `sha256`
-ne bougent pas. (C'est ce qui a été fait pour `~ynh2` et `~ynh3`.)
-
-La suite décrit le premier cas.
-
-## B.2 Compiler et vérifier la nouvelle version
-
-Le code est prêt et committé dans `mabibli`. Depuis `mabibli_ynh` :
-
-```bash
-./build/publier-release.sh --version 0.1.1
-```
-
-⚠️ **`--version` est obligatoire ici.** Sans lui, le script déduit la version de
-`manifest.toml` et reproduirait 0.1.0. Il remet aussi le suffixe à `~ynh1` : une
-nouvelle version applicative repart toujours de 1.
-
-Note le `sha256` affiché — il ne sera plus jamais le même, même à code identique si les
-dépendances bougent.
-
-## B.3 Tag et release
-
-```bash
-cd ../mabibli && git tag v0.1.1 && git push origin main --tags
-```
-
-Créer la release `v0.1.1` dans Gitea, y téléverser
-`build/dist/mabibli-0.1.1-linux-x64.tar.gz`, puis vérifier sans authentification :
-
-```bash
-curl -fsSLI "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v0.1.1/mabibli-0.1.1-linux-x64.tar.gz" | head -1
-```
-
-## B.4 Pousser le paquet
-
-```bash
-cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.1" && git push
-```
-
-## B.5 Sauvegarder, puis mettre à jour
-
-YunoHost prend **lui-même** une sauvegarde de sécurité avant la mise à jour
-(`mabibli-pre-upgrade1`), mais elle **ne contient pas le répertoire de données**
-(`BACKUP_CORE_ONLY`) : elle sert à restaurer l'application, pas la bibliothèque. Prendre
-une sauvegarde complète reste donc utile avant une version qui touche au schéma :
-
-```bash
-sudo yunohost backup create --apps mabibli
-```
-
-```bash
-sudo yunohost app upgrade mabibli -u https://git.akbar.nohost.me/mathieu/mabibli_ynh --debug
-```
-
-⚠️ **L'option `-u` n'est pas facultative ici.** MaBibli n'est pas dans le catalogue
-officiel : sans elle, YunoHost ne sait pas où retrouver le paquet et refuse d'emblée —
-« mabibli is not in the catalog (anymore?) », puis « No apps can be upgraded ». Rien
-n'est cassé pour autant, la commande n'a simplement pas commencé. C'est la **même URL**
-qu'à l'installation, celle du dépôt `_ynh`, jamais celle de l'archive.
-
-(Ajouter `--force` seulement pour réappliquer une version identique, par exemple en
-mise au point du paquet.)
-
-Le script arrête le service **avant** de remplacer les binaires — deux processus sur la
-même base SQLite pendant une migration est exactement ce qu'il faut éviter — puis
-attend `Application started`, avec un délai de 120 s : une migration sur base remplie
-prend plus de temps que la création d'un schéma vide.
-
-## B.6 Vérifier après mise à jour
-
-```bash
-sudo systemctl status mabibli --no-pager && sudo journalctl -u mabibli -n 30 --no-pager
-```
-
-Puis, dans le navigateur, le contrôle qui compte vraiment : **les livres sont toujours
-là**. C'est ce qui valide que la base vit bien dans le répertoire de données et non à
-côté du binaire — `upgrade` remplace intégralement `/var/www/mabibli`.
-
-Vider le cache du navigateur n'est pas nécessaire : le service worker compare les
-empreintes et propose « Mettre à jour ». Sur mobile, un onglet resté ouvert peut
-retarder la bascule — c'est précisément ce que le bandeau de mise à jour sert à
-débloquer.
-
-## B.7 Si la mise à jour échoue
-
-YunoHost restaure automatiquement la sauvegarde de sécurité quand le script échoue. Si
-le service démarre mais que l'application se comporte mal, revenir en arrière à la main :
-
-```bash
-sudo yunohost backup list
-```
-
-```bash
-sudo yunohost app remove mabibli --purge
-```
-
-```bash
-sudo yunohost backup restore --apps mabibli
-```
-
-⚠️ `--purge` efface le répertoire de données. Ne le faire qu'avec une archive contenant
-la bibliothèque sous la main — celle de B.5, pas `mabibli-pre-upgrade1`.
-
----
-
-# C. Désinstaller
-
-```bash
-sudo yunohost app remove mabibli
-```
-
-Retire le service, la conf nginx, la permission SSO, l'utilisateur système, le port et
-`/var/www/mabibli`. **`/home/yunohost.app/mabibli` survit**, donc la bibliothèque aussi :
-une désinstallation faite trop vite ne doit pas être irréversible.
-
-⚠️ Corollaire à connaître en phase d'essai : réinstaller après un `remove` sans purge
-**retrouve l'ancienne base**. Ce n'est pas une installation vierge, même si tout le
-reste est neuf.
-
-Pour tout effacer, données comprises :
-
-```bash
-sudo yunohost app remove mabibli --purge
-```
-
-Contrôle qu'il ne reste rien :
-
-```bash
-systemctl status mabibli; sudo ls -d /var/www/mabibli /home/yunohost.app/mabibli 2>&1; getent passwd mabibli
-```
-
-Les trois doivent être négatifs. Le domaine, lui, reste déclaré dans YunoHost.
diff --git a/README.md b/README.md
index 79ae86f..5ff51bd 100644
--- a/README.md
+++ b/README.md
@@ -4,74 +4,32 @@ Paquet d'installation de [MaBibli](https://git.akbar.nohost.me/mathieu/mabibli),
gestion de bibliothèque personnelle (catalogue, prêts, scan ISBN).
Ce dépôt **ne contient aucun code C#**. Il porte uniquement de quoi installer sur YunoHost une
-archive déjà compilée : le manifeste, les configurations nginx et systemd, et les scripts
-d'installation.
+archive déjà compilée : le manifeste, les configurations nginx et systemd, les scripts
+d'installation, et les scripts de publication.
-## Contenu
+## 📖 Toute la documentation est dans le dépôt du code
-| Fichier | Rôle |
-|---|---|
-| `manifest.toml` | Identité, version, URL de l'archive et son `sha256`, ressources (utilisateur système, répertoires, port, permissions) |
-| `conf/systemd.service` | Unité du service — écoute sur `127.0.0.1`, base dans le répertoire de données, durcissement |
-| `conf/nginx.conf` | Reverse proxy et intégration SSOwat |
-| `scripts/_common.sh` | Variables partagées et sauvegarde/restauration cohérente de la base SQLite |
-| `scripts/install` `remove` `upgrade` `backup` `restore` | Cycle de vie de l'application |
-| `build/publier-release.sh` | Compile, archive, calcule le `sha256` et met à jour `manifest.toml` |
+Elle a été regroupée dans **[le README de `mabibli`](https://git.akbar.nohost.me/mathieu/mabibli#readme)**,
+qui est le point d'entrée unique du projet : contenu et points de conception du paquet,
+publication d'une version, mise en production pas à pas, montée de version, retour arrière,
+désinstallation, et contraintes permanentes.
+
+Il n'y a qu'une seule chaîne de publication et un seul projet : deux jeux de documentation
+finissaient par diverger, et l'on ne savait plus lequel faisait autorité.
+
+Ne restent ici que les deux fichiers que **YunoHost lit lui-même** et qui doivent donc vivre
+à côté du manifeste :
+
+- `doc/DESCRIPTION.md` — présentation affichée dans le catalogue d'applications
+- `doc/ADMIN.md` — authentification, emplacement des données, sauvegarde ; affichée dans
+ l'interface d'administration une fois l'application installée
## Publier une nouvelle version
-Le serveur ne compile jamais : il télécharge une archive déjà produite. La chaîne est donc
-« compiler ici, déposer une release, mettre à jour le manifeste ».
-
```bash
# Depuis ce dépôt, avec le dépôt du code à côté (../mabibli)
-./build/publier-release.sh --version 0.1.1
+./build/publier.sh
```
-Le script :
-
-1. lance `dotnet publish MaBibli.Api -c Release -r linux-x64 --self-contained` ;
-2. vérifie que le binaire est là, que `wwwroot/` a bien été embarqué, et que **toutes** les
- ressources référencées par `index.html` existent réellement dans le publish ;
-3. produit `build/dist/mabibli--linux-x64.tar.gz`, de façon reproductible ;
-4. en calcule le `sha256` ;
-5. réécrit `version`, `amd64.url` et `amd64.sha256` dans `manifest.toml`.
-
-Restent à faire à la main : créer le tag et la release sur Gitea, y téléverser l'archive, puis
-committer et **pousser** `manifest.toml` — YunoHost lit le manifeste depuis Gitea, pas la copie
-locale.
-
-📖 **La marche à suivre complète est dans [`PUBLICATION.md`](PUBLICATION.md)** : première mise en
-production, montée de version (avec les contrôles à chaque étape), retour arrière, et
-désinstallation.
-
-Le script ne dépend d'aucun environnement de CI : tout passe par des options ou des variables
-d'environnement (`MABIBLI_SOURCE_DIR`, `MABIBLI_VERSION`, `MABIBLI_BASE_URL`,
-`MABIBLI_OUTPUT_DIR`), il ne pose aucune question et s'arrête à la première erreur. Il est donc
-utilisable tel quel dans Gitea Actions le jour où un runner sera disponible.
-
-## Points de conception
-
-**Le service n'écoute que sur `127.0.0.1`.** L'application déduit l'identité de l'en-tête
-`YNH_USER` injecté par SSOwat ; cet en-tête n'est digne de confiance que si nginx est le seul
-chemin d'accès. Un service exposé sur le réseau permettrait de forger `YNH_USER` et de
-contourner le portail. La contrainte est écrite dans `conf/systemd.service` (`ASPNETCORE_URLS`)
-et le port n'est pas ouvert au pare-feu.
-
-**Aucune compilation sur le serveur.** Le publish est *self-contained* : il embarque son propre
-runtime .NET, donc aucun paquet `dotnet-runtime` n'est nécessaire. `ynh_setup_source` vérifie le
-`sha256` de l'archive avant de la déployer.
-
-**Les données survivent aux mises à jour.** La base SQLite vit dans le répertoire de données, pas
-à côté du binaire ; `upgrade` ne remplace que le répertoire d'installation.
-
-**La sauvegarde passe par l'API de sauvegarde en ligne de SQLite.** La base est en mode WAL :
-copier le seul fichier `.db` d'une base active peut ne rien sauvegarder du tout. Voir
-`doc/ADMIN.md`.
-
-## Documentation
-
-- `doc/DESCRIPTION.md` — présentation affichée dans le catalogue YunoHost
-- `doc/ADMIN.md` — authentification, emplacement des données, sauvegarde, contraintes
-- `PUBLICATION.md` — mise en production et montée de version, pas à pas
-- `A_FAIRE.md` — contraintes permanentes et journal des points réglés
+Le détail — ce que le script vérifie, ce qui reste manuel, et la marche à suivre côté serveur —
+est dans le README de `mabibli`.
diff --git a/build/publier.sh b/build/publier.sh
new file mode 100755
index 0000000..4404e1d
--- /dev/null
+++ b/build/publier.sh
@@ -0,0 +1,223 @@
+#!/bin/bash
+#
+# Publie une version de MaBibli de bout en bout, en une commande.
+#
+# Enchaîne ce qui était jusqu'ici tapé à la main, toujours dans le même ordre :
+#
+# 1. produire l'archive et mettre à jour manifest.toml (publier-release.sh)
+# 2. committer et pousser mabibli_ynh (le serveur lit Gitea, pas le local)
+# 3. taguer et pousser mabibli (le tag porte le code de la release)
+#
+# La version se choisit ici, à partir de celle déjà publiée : rien à aller chercher.
+# La seule chose qui reste manuelle est le dépôt de l'archive dans la release Gitea —
+# l'URL est rappelée en fin de sortie.
+#
+set -euo pipefail
+
+racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+manifeste="$racine_paquet/manifest.toml"
+
+source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
+base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
+url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}"
+version=""
+increment=""
+sans_confirmation=0
+
+usage() {
+ cat <<'FIN'
+Usage : publier.sh [options]
+
+ --version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente)
+ --patch Incrémenter le dernier chiffre (0.3.0 -> 0.3.1)
+ --minor Incrémenter le chiffre du milieu (0.3.0 -> 0.4.0)
+ --major Incrémenter le premier chiffre (0.3.0 -> 1.0.0)
+ --oui Ne rien demander (pour une CI)
+ -h, --help Cette aide
+FIN
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --version) version="$2"; shift 2 ;;
+ --patch|--minor|--major) increment="${1#--}"; shift ;;
+ --oui|-y) sans_confirmation=1; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
+ esac
+done
+
+echoerr() { echo "$@" >&2; }
+
+if [ -z "$source_dir" ] || [ ! -d "$source_dir/.git" ]; then
+ echoerr "Dépôt du code introuvable : '${source_dir:-}' (MABIBLI_SOURCE_DIR)."
+ exit 1
+fi
+
+#=================================================
+# LA VERSION PRÉCÉDENTE, DEUX SOURCES QUI DOIVENT S'ACCORDER
+#=================================================
+
+# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
+# dernière version *empaquetée*, qui peut être en retard si une publication a échoué en
+# cours de route. On prend le plus élevé des deux pour ne jamais proposer un numéro déjà pris.
+version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | head -1 | sed 's/^v//')"
+version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
+
+precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \
+ | grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1)"
+
+if [ -z "$precedente" ]; then
+ echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
+ exit 1
+fi
+
+echo "Dernière version publiée : $precedente"
+[ "$version_tag" != "$version_manifeste" ] &&
+ echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent"
+
+suivante() { # suivante
+ IFS=. read -r maj min cor <<<"$1"
+ case "$2" in
+ major) echo "$((maj + 1)).0.0" ;;
+ minor) echo "$maj.$((min + 1)).0" ;;
+ patch) echo "$maj.$min.$((cor + 1))" ;;
+ esac
+}
+
+if [ -z "$version" ] && [ -n "$increment" ]; then
+ version="$(suivante "$precedente" "$increment")"
+fi
+
+if [ -z "$version" ]; then
+ if [ "$sans_confirmation" -eq 1 ] || [ ! -t 0 ]; then
+ echoerr "Pas de terminal pour demander la version ; utilisez --version ou --patch/--minor/--major."
+ exit 1
+ fi
+ echo
+ echo " 1) $(suivante "$precedente" patch) (correction)"
+ echo " 2) $(suivante "$precedente" minor) (nouveautés)"
+ echo " 3) $(suivante "$precedente" major) (rupture)"
+ echo " 4) autre — à saisir"
+ echo
+ read -rp "Version à publier [1] ? " choix
+ case "${choix:-1}" in
+ 1) version="$(suivante "$precedente" patch)" ;;
+ 2) version="$(suivante "$precedente" minor)" ;;
+ 3) version="$(suivante "$precedente" major)" ;;
+ 4) read -rp "Numéro de version : " version ;;
+ *) version="$choix" ;; # un numéro tapé directement passe aussi
+ esac
+fi
+
+if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echoerr "Version invalide : '$version' (attendu X.Y.Z)."
+ exit 1
+fi
+
+if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
+ echoerr "Le tag v$version existe déjà dans $source_dir."
+ exit 1
+fi
+
+#=================================================
+# CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER
+#=================================================
+
+# Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc
+# pas dans la release, alors même que l'archive, elle, le contiendrait.
+if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
+ echoerr "Le dépôt du code a des modifications non committées :"
+ git -C "$source_dir" status --short >&2
+ exit 1
+fi
+
+branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
+
+echo
+echo "=== À publier ==="
+echo " version : $precedente -> $version"
+echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
+echo " paquet : $racine_paquet"
+
+if [ "$sans_confirmation" -ne 1 ]; then
+ read -rp "Continuer ? [O/n] " reponse
+ case "${reponse:-o}" in
+ [oO]*) ;;
+ *) echo "Abandon."; exit 1 ;;
+ esac
+fi
+
+#=================================================
+# 1. ARCHIVE + MANIFESTE
+#=================================================
+
+"$racine_paquet/build/publier-release.sh" --version "$version" --source-dir "$source_dir" --base-url "$base_url"
+
+archive_nom="mabibli-$version-linux-x64.tar.gz"
+archive="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}/$archive_nom"
+
+#=================================================
+# 2. LE PAQUET (le serveur lit Gitea, jamais la copie locale)
+#=================================================
+
+echo
+echo "=== Paquet : commit et push ==="
+git -C "$racine_paquet" add -A
+if git -C "$racine_paquet" diff --cached --quiet; then
+ echo " rien à committer (manifeste déjà à jour)"
+else
+ git -C "$racine_paquet" commit -m "Publier la version $version"
+fi
+git -C "$racine_paquet" push
+
+#=================================================
+# 3. LE CODE : tag et push
+#=================================================
+
+echo
+echo "=== Code : tag v$version et push ==="
+git -C "$source_dir" tag "v$version"
+git -C "$source_dir" push origin "$branche" --tags
+
+# Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant.
+verifier_pousse() { # verifier_pousse [
+ if git -C "$1" ls-remote --exit-code origin "$2" >/dev/null 2>&1; then
+ echo " ✓ $2 présent sur origin"
+ else
+ echoerr " ✗ $2 ABSENT sur origin — le push n'a pas abouti."
+ exit 1
+ fi
+}
+
+echo
+echo "=== Vérification côté distant ==="
+verifier_pousse "$source_dir" "refs/tags/v$version"
+verifier_pousse "$racine_paquet" "refs/heads/$(git -C "$racine_paquet" rev-parse --abbrev-ref HEAD)"
+
+#=================================================
+# CE QUI RESTE À FAIRE À LA MAIN
+#=================================================
+
+url_archive="$base_url/v$version/$archive_nom"
+
+cat <]