Sync ia_dev: token resolution via .secrets/<env>/ia_token, doc updates
**Motivations:** - Align master with current codebase (token from projects/<id>/.secrets/<env>/ia_token) - Id resolution by mail To or by API token; no slug **Root causes:** - Token moved from conf.json to .secrets/<env>/ia_token; env from directory name **Correctifs:** - Server and scripts resolve project+env by scanning all projects and envs **Evolutions:** - tickets-fetch-inbox routes by To address; notary-ai agents and API doc updated **Pages affectées:** - ai_working_help/server.js, docs, project_config.py, lib/project_config.sh - projects/README.md, lecoffreio/docs/API.md, gitea-issues/tickets-fetch-inbox.py
This commit is contained in:
commit
61cec6f430
83
.cursor/agents/agent-loop.md
Normal file
83
.cursor/agents/agent-loop.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: agent-loop
|
||||||
|
description: Orchestre la boucle de récupération des mails et le traitement par gitea-issues-process. Gère les boucles par exécutions délimitées uniquement (N itérations ou x cycles) ; ne jamais lancer de processus en arrière-plan (nohup / &).
|
||||||
|
model: inherit
|
||||||
|
is_background: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent agent-loop
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` ; `<id>` = contenu du fichier `../ai_project_id` (à la racine du dépôt projet, parent de ia_dev). Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test` (ou `..` depuis le workspace ia_dev). Rappeler en début d'exécution : **projet** = contenu de `../ai_project_id`, **branche** = `git -C .. branch --show-current`, **répertoire de travail** = répertoire du dépôt dans `../`.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, projet, branche, répertoire de travail du dépôt dans `../`.
|
||||||
|
|
||||||
|
Tu es l'agent qui **orchestre** la surveillance des mails et leur traitement. Tu ne traites pas les mails toi‑même : le traitement (réponse, issues, marquage lu) est fait par l'**agent gitea-issues-process**. Tu lances les scripts et/ou les sous-agents selon la demande.
|
||||||
|
|
||||||
|
**Références obligatoires** : lire `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (contexte d'exécution). Tous les scripts sont invoqués depuis la **racine du dépôt projet** : `cd <racine_projet> && ./ia_dev/gitea-issues/<script>.sh` (depuis le workspace ia_dev : `cd .. && ./ia_dev/gitea-issues/<script>.sh`).
|
||||||
|
|
||||||
|
**Script agent-loop.sh** : intervalle en premier argument (ex. `./ia_dev/gitea-issues/agent-loop.sh 50` = 50 s). La boucle utilise le **spooler** (critère from/to dans conf.json), pas le statut IMAP « non lu ». Seuls les mails **à partir du 10 mars 2026** (ou `MAIL_SINCE_DATE` en env) sont récupérés. **Fichier témoin** `projects/<id>/logs/gitea-issues/agent-loop.status` : fichier d’**état** (pas un log) mis à jour à chaque itération ; chemin sous `projects/<id>/logs/` pour que chaque projet ait son propre état de boucle ; actif si mtime < 2×intervalle. Fichier pending : `projects/<id>/logs/gitea-issues/agent-loop.pending` (chemins des .pending du spooler à traiter). Variables (optionnel `.secrets/gitea-issues/agent-loop.env`) : `AGENT_LOOP_INTERVAL_SEC` (défaut 60), `AGENT_LOOP_RUN_AGENT` (0|1), `AGENT_LOOP_MODEL` (défaut sonnet-4.6), `AGENT_LOOP_STATUS_FILE`, `AGENT_LOOP_PENDING_FILE`. Hook « remonter mails » : `.cursor/hooks/remonter-mails.sh` lit `projects/<id>/data/issues/*.pending` ou `agent-loop.pending`.
|
||||||
|
|
||||||
|
**Fichiers de contrôle (section 2 — boucle x cycles)** :
|
||||||
|
- **Lock (une seule instance)** : `projects/<id>/logs/gitea-issues/agent-loop.lock`. Utiliser les scripts : **agent-loop-lock-acquire.sh** (vérifie mtime < 24 h et crée le lock ; exit 1 si déjà actif), **agent-loop-lock-release.sh** (supprime lock et fichier stop).
|
||||||
|
- **Arrêt à la demande** : `projects/<id>/logs/gitea-issues/agent-loop.stop`. Au début de chaque cycle, exécuter **agent-loop-stop-requested.sh** (exit 0 si le fichier existe) ; si oui, lancer **agent-loop-lock-release.sh** et sortir. Pour demander l'arrêt : `cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh` ou `touch projects/<id>/logs/gitea-issues/agent-loop.stop`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Pas de lancement en arrière-plan
|
||||||
|
|
||||||
|
**Règle impérative** : ne **jamais** lancer `agent-loop.sh` ni `agent-loop-treatment.sh` en arrière-plan (pas de `nohup`, pas de `&`). Ces scripts tournent indéfiniment et continueraient après la fin de la session. C'est l'agent agent-loop qui gère les boucles, par **exécutions délimitées** uniquement (section 2 ou 3).
|
||||||
|
|
||||||
|
Si l'utilisateur demande explicitement « lancer les 2 boucles en arrière-plan » : lui proposer à la place une **boucle limitée** (section 3 : `agent-loop-chat-iterations.sh [N]` avec N choisi, ou section 2 : x cycles récupération + traitement). Ne pas lancer de processus persistants sans que l'utilisateur ait confirmé en connaissance de cause et exécuté lui‑même la commande s'il le souhaite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Lancer x fois : récupération 1 fois puis traitement 1 fois (x cycles)
|
||||||
|
|
||||||
|
Si l'utilisateur demande de **lancer x fois** les deux sous-agents (une récupération puis un traitement, répété x fois) :
|
||||||
|
|
||||||
|
**Avant de commencer les x cycles** :
|
||||||
|
- Exécuter depuis la racine du dépôt : `cd .. && ./ia_dev/gitea-issues/agent-loop-lock-acquire.sh`.
|
||||||
|
- Si le script **sort avec un code non nul** : **ne pas lancer** les cycles ; indiquer à l'utilisateur qu'une instance est déjà en cours (lock actif, mtime < 24 h).
|
||||||
|
- Si le script sort 0 : le lock est acquis ; poursuivre.
|
||||||
|
- **À la fin** (normale ou après arrêt) : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-lock-release.sh` (supprime le lock et le fichier stop s'il existe).
|
||||||
|
|
||||||
|
Pour chaque cycle `i` de 1 à x :
|
||||||
|
|
||||||
|
**Au début du cycle** (avant l'étape 1) : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-stop-requested.sh`. Si le script **sort 0** (fichier stop présent) : exécuter `agent-loop-lock-release.sh`, puis **sortir** en indiquant que la boucle a été arrêtée à la demande.
|
||||||
|
|
||||||
|
1. **Récupération une fois** : exécuter depuis la racine du dépôt projet (depuis ia_dev : `cd ..`) :
|
||||||
|
```bash
|
||||||
|
cd .. && ./ia_dev/gitea-issues/agent-loop-retrieval-once.sh
|
||||||
|
```
|
||||||
|
Ce script exécute `tickets-fetch-inbox.sh` puis `list-pending-spooler.sh` et écrit les chemins des .pending dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (et met à jour le fichier témoin). Pas d'arrière-plan : attendre la fin du script.
|
||||||
|
|
||||||
|
2. **Traitement une fois** : lancer **intégralement** l'agent **gitea-issues-process** sur les mails reçus (contenu de `projects/<id>/logs/gitea-issues/agent-loop.pending`). Utiliser le sous-agent Cursor (mcp_task ou équivalent) avec le type `gitea-issues-process` et un prompt du type :
|
||||||
|
« Traite les mails du spooler listés dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (chemins des .pending) ou en exécutant `list-pending-spooler.sh`. Pour chaque fichier .pending : lire le JSON, rédiger une réponse pertinente (uniquement ton texte ; pas de citation — mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Appeler mail-send-reply.sh puis write-response-spooler.sh. Ne pas appeler mail-mark-read.sh (spooler). »
|
||||||
|
|
||||||
|
3. **Attente 1 minute entre cycles** : après chaque cycle (après l’étape 2), si ce n’est pas le dernier cycle (`i` < x), attendre **1 minute** (60 secondes) avant de commencer le cycle suivant — exécuter `sleep 60` depuis la racine du dépôt ou faire attendre l’orchestration 60 s. Pas d’attente après le dernier cycle.
|
||||||
|
|
||||||
|
Répéter les étapes 1, 2 et 3 pour les x cycles demandés. Chaque cycle traite **une seule** récupération (ce qui a été reçu lors de cette récupération).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Autres demandes
|
||||||
|
|
||||||
|
- **Boucle limitée depuis le chat (N itérations, attente 1 min entre chaque)** : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]` (sans arrière-plan, pour éviter timeout). Par défaut N=3 ; `--repeat` pour relancer après N itérations.
|
||||||
|
- **Arrêter la boucle en cours (section 2)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh`. Cela crée le fichier `agent-loop.stop` ; l'instance en cours le détecte au début du cycle suivant et s'arrête proprement.
|
||||||
|
- **Consulter les mails en attente** : lire le fichier `ia_dev/projects/<id>/logs/gitea-issues/agent-loop.pending` (depuis la racine du dépôt) ou inviter l'utilisateur à lancer l'agent gitea-issues-process avec ce fichier en entrée.
|
||||||
|
- **Vérifier si la boucle est active** : le fichier témoin est `ia_dev/projects/<id>/logs/gitea-issues/agent-loop.status` ; s'il a été modifié depuis moins de 2 × intervalle (ex. 120 s si intervalle 60 s), la boucle est considérée active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- **Pas de processus en arrière-plan** : ne jamais lancer `agent-loop.sh` ni `agent-loop-treatment.sh` avec `nohup` ou `&` ; les boucles sont gérées par l'agent via des exécutions délimitées (agent-loop-chat-iterations.sh N, ou cycles section 2).
|
||||||
|
- Ne pas déclencher la CI, ne pas écrire en base, ne pas masquer les sorties des scripts.
|
||||||
|
- Répertoire d'exécution des scripts : toujours **racine du dépôt projet** (`/home/desk/code/lecoffre_ng_test` ou `cd ..` depuis ia_dev).
|
||||||
|
- Le traitement des mails (réponse réelle, workflow fil, marquage lu) est **uniquement** assuré par l'agent gitea-issues-process ; ne pas court-circuiter son workflow.
|
||||||
|
|
||||||
|
## Clôture
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc` en fin de réponse (horodatage, projet, branche, répertoire, points 1 à 19).
|
||||||
59
.cursor/agents/branch-align-by-script-from-test.md
Normal file
59
.cursor/agents/branch-align-by-script-from-test.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: branch-align-by-script-from-test
|
||||||
|
description: Exécute le script branch-align du projet pour aligner les branches distantes test, pprod et prod à partir de l'environnement courant (main, test, pprod ou prod, par défaut test). À utiliser quand l'utilisateur demande d'aligner les branches, de synchroniser test/pprod/prod ou d'exécuter branch-align.sh.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent branch-align-by-script-from-test
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu alignes les branches distantes du projet (test, pprod, prod) en exécutant le script deploy/branch-align.sh avec **test** en paramètre par défaut. **Rôle de l’agent :** vérifications (prérequis), ordre des étapes (push → docupdate → script), relances en cas d’erreur remontée par les sous-agents, synthèse et clôture. **Rôle du script :** exécution déterministe (fetch, force-push, vérifications git). Push direct sur les branches distantes ; aucun script ni agent ne crée de pull request.
|
||||||
|
|
||||||
|
**Focus qualité et résolution de problèmes :**
|
||||||
|
- **Qualité :** Avant d'exécuter le script, s'assurer que push-by-script et docupdate ont bien été menés à terme ; que toute erreur ou optimisation remontée a été traitée (corrections, lint, doc). Ne pas lancer branch-align sur un état non committé ou non documenté.
|
||||||
|
- **Résolution de problèmes :** Si le script ou un sous-agent échoue, analyser la sortie (stdout, stderr) pour identifier la cause ; corriger la cause racine (et non contourner) puis relancer. Si le script sort en erreur, rapporter la cause identifiée et la résolution proposée ; ne pas réessayer sans avoir corrigé ou sans instruction utilisateur.
|
||||||
|
|
||||||
|
- **Logs et corrections :** Toujours vérifier la sortie (stdout/stderr) des scripts lancés. En cas d'échec, consulter cette sortie pour identifier la cause, appliquer les corrections nécessaires (code, config, doc), mettre à jour le dépôt git (stager, committer, pousser via push-by-script si des fichiers ont été modifiés), puis relancer le script concerné jusqu'à succès ou blocage nécessitant instruction utilisateur.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents). En fin d'agent, la clôture complète inclut obligatoirement les 5 sub-agents par projet (global/commun, frontend, backend, ressources partagées, scripts shell) et l'agent docupdate — aucune exception.
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/branch-align.sh`).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
Lors de l'invocation :
|
||||||
|
|
||||||
|
1. Le script branch-align.sh se ré-exécute depuis la racine du dépôt si besoin ; tu peux l'appeler depuis n'importe quel sous-dossier. Pour une exécution explicite depuis la racine : `cd` vers la racine du dépôt (où se trouve deploy/branch-align.sh) avant d'exécuter le script si tu préfères.
|
||||||
|
|
||||||
|
2. **Exécuter obligatoirement et intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) **systématiquement**, même s'il n'y a rien à committer (au pire fournir un message de commit avec tous les critères vides mais essaie toujours de remplir tous les champs attendus par `.cursor/agents/push-by-script.md` ).
|
||||||
|
|
||||||
|
3. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : **traiter obligatoirement** (identifier la cause, corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée. Ne pas relancer sans avoir traité la cause.
|
||||||
|
|
||||||
|
|
||||||
|
4. Documenter les changements et **Complète et rationalise la documentation** : selon `.cursor/agents/docupdate.md`, documenter puis **lancer et exécuter intégralement** l'agent `.cursor/agents/docupdate.md` (commande /docupdate).
|
||||||
|
|
||||||
|
5. Puis exécuter depuis la racine du dépôt projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/branch-align.sh <env>`. Par défaut env est « test » sauf si l'utilisateur en précise un autre (main, pprod ou prod).
|
||||||
|
|
||||||
|
6. Le **script** branch-align.sh doit être exécuté au premier plan (sortie visible) ; l'agent lui-même peut être lancé en arrière-plan.
|
||||||
|
|
||||||
|
7. Ne pas masquer ni filtrer la sortie du script.
|
||||||
|
|
||||||
|
Prérequis (imposés par le script) :
|
||||||
|
|
||||||
|
- Être dans un dépôt git.
|
||||||
|
- Être sur la branche correspondant à l'env (ex. sur main quand env est main, sur test quand env est test). Demander une confirmation quand ce n'est pas *test*.
|
||||||
|
- Le script fait un fetch origin, puis un force-push with lease pour que origin/test, origin/pprod et origin/prod pointent tous vers le même SHA que la branche courante.
|
||||||
|
|
||||||
|
## Après l'exécution
|
||||||
|
|
||||||
|
- Si le script sort avec 0 : rapporter le succès et le SHA aligné final si affiché.
|
||||||
|
- Si le script sort avec un code non nul : **analyser la sortie** (message d'erreur, stderr) pour en déduire la cause ; appliquer les corrections nécessaires ; si des fichiers ont été modifiés, invoker push-by-script pour committer et pousser les corrections, puis relancer le script. Rapporter la cause identifiée et la résolution appliquée. Ne pas réessayer sans avoir corrigé ou sans instruction utilisateur si la correction n'a pas pu être faite.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
31
.cursor/agents/change-to-all-branches.md
Normal file
31
.cursor/agents/change-to-all-branches.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: change-to-all-branches
|
||||||
|
model: inherit
|
||||||
|
description: Uniquement en test, lance /push-by-script puis deploy/change-to-all-branches.sh (alignement + déploiement test).
|
||||||
|
---
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
**Rôle de l’agent :** vérifier que la branche locale est test (sinon retour 1), fournir le message de commit (via /push-by-script), lancer le script, contrôler la sortie et le code de retour. **Rôle du script :** exécution déterministe (vérif branche test, branch-align.sh test, deploy.sh test --import-v1 --skipSetupHost --no-sync-origin ; log dans logs/ par défaut).
|
||||||
|
|
||||||
|
**Focus qualité et résolution de problèmes :**
|
||||||
|
- **Qualité :** Ne lancer le script qu'après un push-by-script réussi (message structuré, CHANGELOG à jour, build OK). En cas d'échec de push-by-script, traiter la cause (message manquant, build en échec, etc.) avant de continuer.
|
||||||
|
- **Résolution de problèmes :** Si change-to-all-branches.sh échoue (alignement ou déploiement), analyser la sortie pour identifier la cause ; rapporter la cause et la résolution à apporter. Ne pas relancer sans avoir corrigé ou sans instruction utilisateur.
|
||||||
|
|
||||||
|
- **Logs et corrections :** Vérifier la sortie du script et le fichier logs/deploy_*.log produit par deploy.sh. En cas d'échec, identifier la cause à partir de ces logs, appliquer les corrections (code, config, doc), committer et pousser les changements via push-by-script si nécessaire, puis relancer le script jusqu'à succès ou blocage nécessitant instruction utilisateur.
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/change-to-all-branches.sh`).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
Si la branche locale actuelle n'est pas test, retourner 1.
|
||||||
|
|
||||||
|
Uniquement en test (branche git), exécuter dans l'ordre :
|
||||||
|
|
||||||
|
1. **/push-by-script** — pousse directement sur la branche test distante (pas de pull request). Message de commit fourni par l'agent.
|
||||||
|
2. **Exécuter depuis la racine projet (chemin absolu)** : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/change-to-all-branches.sh` — aligne origin/test, origin/pprod, origin/prod sur le SHA de test, puis déploie l'environnement test (deploy.sh avec --import-v1 --skipSetupHost --no-sync-origin ; log dans logs/ par défaut). Push direct uniquement ; aucun script ni agent ne crée de pull request.
|
||||||
|
|
||||||
|
Retourner 0 en cas de succès. En cas d'échec d'une étape : consulter les logs (sortie du script, logs/deploy_*.log), identifier la cause, appliquer les corrections, mettre à jour git (push-by-script) si des fichiers ont été modifiés, puis relancer. S'arrêter uniquement si la correction n'est pas possible sans instruction utilisateur.
|
||||||
103
.cursor/agents/code.md
Normal file
103
.cursor/agents/code.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: code
|
||||||
|
description: Règles de qualité du code, patterns, architecture et tests. À appliquer lorsqu'il y a du code à produire (évolutions ou correctifs).
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent code (qualité du code et bonnes pratiques)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu appliques les règles ci-dessous **lorsqu'il y a du code à produire** (évolution ou correctif). Les agents evol et fix invoquent cet agent dans ce cas.
|
||||||
|
|
||||||
|
## Liste ordonnée d'actions obligatoires pour coder
|
||||||
|
|
||||||
|
1. **Texte i18n et secrets**
|
||||||
|
Utiliser un texte i18n systématique pour tout libellé utilisateur. Tenir à jour `.secrets/<env>/env-full-<env>-for-bdd-injection.txt` selon l'environnement.
|
||||||
|
|
||||||
|
2. **Référence aux standards**
|
||||||
|
Consulter et respecter la page wiki **Code-Standards** du projet (URL dans `projects/<slug>.json` → `git.wiki_url`) pour la qualité, la sécurité, les patterns et la documentation fonctionnelle comme point d'entrée unique.
|
||||||
|
|
||||||
|
3. **Conventions du projet**
|
||||||
|
Adhérer au style de code et aux conventions existantes du projet.
|
||||||
|
|
||||||
|
4. **Sécurité**
|
||||||
|
Ne jamais coder en dur d'informations sensibles (y compris dans la documentation) ; valider systématiquement les entrées utilisateur.
|
||||||
|
|
||||||
|
5. **Performances**
|
||||||
|
Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
|
||||||
|
|
||||||
|
6. **Clarté et maintenabilité**
|
||||||
|
S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
|
||||||
|
|
||||||
|
7. **Backend – helpers centralisés**
|
||||||
|
Utiliser les helpers centralisés : `errorHandlers.ts` (handleInternalError, handleValidationError, etc.), `errorLoggers.ts` (logError, logValidationError, etc.), `errorMessages.ts`, `userHelpers.ts` (isSuperAdminUser, extractUserData, etc.).
|
||||||
|
|
||||||
|
8. **Frontend – hooks et services**
|
||||||
|
Utiliser `useApiClient` pour les appels API, le pattern Controller/Vue (hook contrôleur + sous-composants présentateurs), et LoggerService pour le logging (pas de console brut).
|
||||||
|
|
||||||
|
9. **Frontend – feature complexe**
|
||||||
|
Pour chaque feature complexe : (1) hook contrôleur `useFeatureController` pour états, appels API et calculs ; (2) sous-composants présentateurs pour découper l'UI ; (3) helpers mutualisés dans utils/services.
|
||||||
|
|
||||||
|
10. **Environnement – .env**
|
||||||
|
Ne pas modifier les fichiers `.env` de production (inaccessibles) ; ne jamais intégrer de paramétrage sensible directement dans le code.
|
||||||
|
|
||||||
|
11. **Environnement – env.example**
|
||||||
|
Maintenir `env.example` systématiquement à jour.
|
||||||
|
|
||||||
|
12. **Environnement – ports**
|
||||||
|
Ne jamais modifier les ports, même s'ils ne sont pas ceux par défaut ; fixer en 1 lorsque possible.
|
||||||
|
|
||||||
|
13. **Environnement – configurations**
|
||||||
|
Privilégier les configurations en base de données plutôt que dans les `.env`.
|
||||||
|
|
||||||
|
14. **Logging – centralisation**
|
||||||
|
Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement, etc.).
|
||||||
|
|
||||||
|
15. **Logging – système**
|
||||||
|
Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
|
||||||
|
|
||||||
|
16. **Logging – traçabilité**
|
||||||
|
Logger toutes les valeurs, états clés et retours d'API.
|
||||||
|
|
||||||
|
17. **Interactions – base de données**
|
||||||
|
Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
|
||||||
|
|
||||||
|
18. **Interactions – APIs externes**
|
||||||
|
Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
|
||||||
|
|
||||||
|
19. **Interactions – emails**
|
||||||
|
Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
|
||||||
|
|
||||||
|
20. Lancer obligatoirement un lint
|
||||||
|
Utiliser l'agent `.cursor/agents/fix-lint.md` (commande /fix-lint)
|
||||||
|
|
||||||
|
21. **Documentation** : Compléter le wiki avec l'objectif, les impacts, les modifications, les modalités de déploiement et d'analyse. **`docs/` est hors versionnement** : maintenir les fichiers dans `docs/` localement (ne pas les supprimer), puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh` pour pousser vers le wiki ; ou éditer la page wiki directement. Ne pas committer `docs/`. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé de ce qu'il fait, puis l'exécuter.
|
||||||
|
|
||||||
|
22. **Commit** : Préparer le commit avec le format de `.cursor/agents/push-by-script.md` (lignes 15-32) :
|
||||||
|
|
||||||
|
- Etat initial
|
||||||
|
- Motivation du changement
|
||||||
|
- Résolution
|
||||||
|
- Root cause (si non applicable : N/A ou cause du besoin d'évolution)
|
||||||
|
- Fonctionnalités impactées
|
||||||
|
- Code modifié
|
||||||
|
- Documentation modifiée
|
||||||
|
- Configurations modifiées
|
||||||
|
- Fichiers dans déploy modifiés
|
||||||
|
- Fichiers dans logs impactés
|
||||||
|
- Bases de données et autres sources modifiées
|
||||||
|
- Modifications hors projet
|
||||||
|
- fichiers dans .cursor/ modifiés
|
||||||
|
- fichiers dans .secrets/ modifiés
|
||||||
|
- nouvelle sous sous version dans VERSION
|
||||||
|
- CHANGELOG.md mise à jour (oui/non)
|
||||||
|
|
||||||
|
23. **Push** : Lancer et **exécuter intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) avec ce message de commit. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
54
.cursor/agents/deploy-by-script.md
Normal file
54
.cursor/agents/deploy-by-script.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: deploy-by-script
|
||||||
|
description: Lance le déploiement scripts_v2 sur l'environnement courant (branche locale) avec import-v1 et skipSetupHost, après vérification du suivi des branches, sortie vers un log daté.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Déploiement par script (deploy-by-script)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Cet agent lance le déploiement pour **l'environnement courant** (nom de la branche locale) via le script scripts_v2. **Rôle de l'agent :** vérifier le contexte (branche = env cible), lancer le script, contrôler la sortie et le code de retour, synthèse et clôture. **Rôle du script :** exécution et orchestration sûre (suivi branches, sync, log, déploiement).
|
||||||
|
|
||||||
|
**Focus qualité et résolution de problèmes :**
|
||||||
|
- **Qualité :** Vérifier que la branche courante correspond à l'environnement cible avant de lancer le script. Ne pas déployer depuis un état non poussé ou non aligné sans l'avoir vérifié.
|
||||||
|
- **Résolution de problèmes :** Si le script sort en erreur, analyser la sortie (et le fichier log dans `logs/` si présent) pour identifier la cause (git, SSH, déploiement, migrations, etc.) ; rapporter la cause identifiée et la résolution à apporter. Ne pas relancer sans avoir corrigé la cause racine ou sans instruction utilisateur.
|
||||||
|
|
||||||
|
- **Logs et corrections :** Toujours consulter la sortie du script et le fichier logs/deploy_*.log après exécution. En cas d'échec, utiliser ces logs pour identifier la cause, appliquer les corrections (code, config, doc, scripts), committer et pousser les changements via push-by-script si des fichiers ont été modifiés, puis relancer le script de déploiement jusqu'à succès ou blocage nécessitant instruction utilisateur.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/scripts_v2/deploy.sh`).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
**Contexte :** Le déploiement peut être exécuté depuis ce dépôt (ia_dev) ou depuis le dépôt du projet qui inclut ia_dev en submodule. Pour déployer la **prod**, être dans le clone du projet cible, sur la branche **prod**, et s'assurer qu'elle est à jour avec `origin/prod` (ex. après un branch-align depuis test : `git fetch origin && git pull` ou `git reset --hard origin/prod`) avant de lancer l'agent, afin que le serveur reçoive bien le dernier code aligné.
|
||||||
|
|
||||||
|
## 1. Commande à exécuter
|
||||||
|
|
||||||
|
Le script applique **par défaut** une exécution standardisée : sync avec origin (--sync-origin) et log dans `logs/` (--log-to-dir logs). Options --no-sync-origin et --no-log pour désactiver.
|
||||||
|
|
||||||
|
Exécuter depuis la racine du dépôt projet (chemin absolu, pour ne pas dépendre du répertoire courant) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/desk/code/lecoffre_ng_test && ./deploy/scripts_v2/deploy.sh $(git -C /home/desk/code/lecoffre_ng_test branch --show-current) --import-v1 --skipSetupHost
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script fait alors automatiquement : suivi des branches origin, sync de la branche courante avec origin, tee vers `logs/deploy_YYYYMMDD_HHMMSS.log`, puis déploiement.
|
||||||
|
|
||||||
|
## 2. Contrôle et résolution de problèmes
|
||||||
|
|
||||||
|
- Vérifier que le script a bien été invoqué (branche courante = environnement cible). En cas de code de sortie non nul, consulter la sortie et le log (logs/deploy_*.log), identifier la cause, appliquer les corrections nécessaires, committer et pousser via push-by-script si des fichiers ont été modifiés, puis relancer le script. Rapporter la cause et la résolution. Ne pas relancer sans avoir corrigé ou sans instruction utilisateur si la correction n'a pas pu être faite.
|
||||||
|
|
||||||
|
## 3. Après l'exécution
|
||||||
|
|
||||||
|
- Si le script sort avec 0 : rapporter le succès.
|
||||||
|
- Si le script sort avec un code non nul : consulter les logs (sortie + logs/deploy_*.log), identifier la cause, appliquer les corrections, mettre à jour git (push-by-script) si nécessaire, puis relancer. Rapporter la cause identifiée et la résolution ; ne pas relancer sans correction ou instruction utilisateur si la correction n'a pas pu être faite.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
43
.cursor/agents/deploy-pprod-or-prod.md
Normal file
43
.cursor/agents/deploy-pprod-or-prod.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: deploy-pprod-or-prod
|
||||||
|
description: Déploie vers pprod ou prod en suivant le workflow change-to-all-branches, deploy-by-script-to, puis push-by-script. Paramètre obligatoire pprod ou prod.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent deploy-pprod-or-prod
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début et en fin d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
**Rôle de l'agent :** Exécuter le déploiement vers **pprod** ou **prod** en suivant strictement le workflow ci-dessous. Paramètre obligatoire : `pprod` ou `prod`. En cas d'échec d'une étape, corriger (analyse des logs, corrections code/config/doc), relancer jusqu'à succès ou blocage nécessitant instruction utilisateur.
|
||||||
|
|
||||||
|
**Répertoire d'exécution :** Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test`. Tous les scripts sont invoqués après `cd` vers cette racine.
|
||||||
|
|
||||||
|
## Workflow obligatoire
|
||||||
|
|
||||||
|
1. **Vérifier la branche** : La machine doit être sur la branche **test** au démarrage. Si ce n'est pas le cas, indiquer à l'utilisateur de passer sur test (ou exécuter `git checkout test` depuis la racine projet) avant de continuer.
|
||||||
|
|
||||||
|
2. **Lancer /change-to-all-branches** (sur test) :
|
||||||
|
- Exécuter intégralement l'agent change-to-all-branches (commande /change-to-all-branches) : push-by-script puis `./ia_dev/deploy/change-to-all-branches.sh`.
|
||||||
|
- **Si KO :** Analyser la sortie et les logs (logs/deploy_*.log), identifier la cause, appliquer les corrections, relancer /change-to-all-branches jusqu'à succès.
|
||||||
|
- **Si OK :** Passer à l'étape 3.
|
||||||
|
|
||||||
|
3. **Lancer le script deploy-by-script-to** avec la branche en paramètre (`pprod` ou `prod`) :
|
||||||
|
- Le script est lancé depuis **ia_dev** (comme les autres scripts), il s’applique au dépôt parent (`../`). Depuis la racine projet : `cd /home/desk/code/lecoffre_ng_test/ia_dev && ./deploy/deploy-by-script-to.sh <pprod|prod>` (ou depuis la racine : `./ia_dev/deploy/deploy-by-script-to.sh <pprod|prod>`).
|
||||||
|
- Le script fait : passage dans le dépôt parent, checkout sur la branche en paramètre, vérification que `.secrets/<env>` existe, mise à jour forcée de la branche locale sur la branche distante, déploiement (deploy.sh avec --import-v1 --skipSetupHost), checkout test.
|
||||||
|
- **Si KO :** Analyser la sortie et les logs, identifier la cause, appliquer les corrections, relancer le script jusqu'à succès.
|
||||||
|
- **Si OK :** Passer à l'étape 4.
|
||||||
|
|
||||||
|
4. **Checkout test** : Le script remet déjà sur test. Vérifier que la branche courante est test après le script.
|
||||||
|
|
||||||
|
5. **Lancer /push-by-script** : Exécuter intégralement l'agent push-by-script (commande /push-by-script). Message de commit fourni par l'agent selon les règles du projet.
|
||||||
|
|
||||||
|
## Horodatage et contexte
|
||||||
|
|
||||||
|
Appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents). Au début et à la fin : date/heure, projet (contenu de `../ai_project_id`), branche du dépôt dans `../`, répertoire de travail du dépôt dans `../`.
|
||||||
|
|
||||||
|
## Clôture complète
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation.
|
||||||
83
.cursor/agents/docupdate.md
Normal file
83
.cursor/agents/docupdate.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: docupdate
|
||||||
|
description: Met à jour la documentation
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# docupdate
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
Ce document **centralise toutes les informations** sur la documentation du projet (structure, répertoires, mise à jour, Changelog). **L'appel à cet agent** est centralisé dans `.cursor/rules/cloture-evolution.mdc` (étape 12) — à exécuter intégralement lors de la clôture.
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (ex. `gitea-issues/wiki-migrate-docs.sh` lorsqu'il est invoqué).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
## Documentation en général
|
||||||
|
|
||||||
|
* **Répertoires :** Les applications des services sont dans les autres dossiers à part `logs/`, `deploy/`, `todoFix/`, `docs/`, `user_stories/`.
|
||||||
|
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
|
||||||
|
* **Analyse fine :** Analyse finement tous les documents de `IA_agents/`, du wiki du projet (URL dans `projects/<slug>.json` → `git.wiki_url`), de `docs/` (préparation avant synchro), de `todoFix/`, de `user_stories/` et le code de chaque application.
|
||||||
|
* **Analyse fine :** Analyse finement `deploy/scripts/bump-version.sh`.
|
||||||
|
* **Analyse fine :** Analyse finement `deploy/scripts/build-and-deploy.sh`.
|
||||||
|
* **User Stories :** Consulter `user_stories/INDEX.md` pour comprendre les 43 user stories et leurs dépendances. Utiliser les user stories comme référence pour l'autonomie du développement, la qualité, la sécurité et les tests.
|
||||||
|
|
||||||
|
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `docs/todoFix/` et la documentation (wiki).
|
||||||
|
* **Structure de la documentation :**
|
||||||
|
* La documentation générale et pérenne se trouve dans le **wiki** du projet (URL dans `projects/<slug>.json` → `git.wiki_url`). Page d'accueil du wiki : **Home**.
|
||||||
|
* Pour mettre à jour le wiki : modifier le fichier correspondant dans `docs/` puis exécuter depuis la racine projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh` ; ou éditer la page directement sur le wiki. Correspondance fichier → page : voir `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (section Migration docs/ → wiki).
|
||||||
|
* **`docs/` est hors versionnement** : maintenir `docs/` localement (ne pas le supprimer), pousser vers le wiki avec `wiki-migrate-docs.sh` après édition ; ne jamais committer `docs/`.
|
||||||
|
* Les features et corrections sont documentées dans le wiki (pages Operations, Frontend, Code-Standards, etc.) ; les tâches en cours dans `docs/todoFix/`.
|
||||||
|
* Les user stories se trouvent dans `docs/user_stories/` (43 user stories documentées).
|
||||||
|
* **User Stories :** Consulter `docs/user_stories/INDEX.md` pour la liste complète et les dépendances. Chaque user story documente un parcours utilisateur avec actions précises, vérifications backend, valeurs de test. Utiliser comme référence pour l'autonomie du développement.
|
||||||
|
* **Qualité et sécurité :** Consulter les pages wiki correspondantes (ex. Code-Standards) ou `docs/` si présents.
|
||||||
|
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents sans raison ; enrichir et mettre à jour le wiki (ou docs/ puis wiki-migrate-docs.sh).
|
||||||
|
* **Mise à jour continue :** Mettre à jour la documentation (wiki via docs/ et `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, `docs/todoFix/`, `docs/user_stories/` et commentaires dans le code) après les modifications ou pour clarifier.
|
||||||
|
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les modifications majeures. Ce contenu est repris dans la splash notice de l'application front. Les mises à jour mineures sont ajoutées au `CHANGELOG.md` sans enlever d'élément existant.
|
||||||
|
|
||||||
|
## docs/features extract
|
||||||
|
|
||||||
|
Dans l'ordre et pour tous les documents de docs/features :
|
||||||
|
|
||||||
|
1) Extraire toutes les données pertinentes des documents de docs/features et les intégrer dans les pages wiki existantes (mettre à jour les fichiers correspondants dans docs/ puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`).
|
||||||
|
|
||||||
|
2) Supprimer tous les fichiers dans docs/features
|
||||||
|
|
||||||
|
## docs/fixKnowledge extract
|
||||||
|
|
||||||
|
Dans l'ordre et pour tous les documents de docs/fixKnowledge :
|
||||||
|
|
||||||
|
1) Extraire toutes les données pertinentes des documents de docs/fixKnowledge et les intégrer dans les pages wiki existantes (mettre à jour docs/ puis `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`).
|
||||||
|
|
||||||
|
2) Supprimer tous les fichiers dans docs/fixKnowledge
|
||||||
|
|
||||||
|
## docs/ et wiki cleanup
|
||||||
|
|
||||||
|
Dans l'ordre et pour tous les documents de docs et les pages wiki :
|
||||||
|
|
||||||
|
Documents / pages à ne pas supprimer lors des étapes suivantes (équivalents wiki des anciens fichiers docs/) :
|
||||||
|
|
||||||
|
* Page wiki Home (page d'accueil du wiki)
|
||||||
|
* Pages wiki : Api, Architecture, Code-Standards, Deployment, Operations, Readme, Scripts, etc. (voir projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md pour la correspondance complète).
|
||||||
|
* docs/sources/*
|
||||||
|
* docs/fixKnowledge/*
|
||||||
|
* docs/features/*
|
||||||
|
|
||||||
|
1) Réunir et optimiser la documentation (wiki) en maximum 20 pages markdown
|
||||||
|
|
||||||
|
2) Supprimer les informations fausses ou obsolètes
|
||||||
|
|
||||||
|
Ventiler les infos de features dans les pages wiki existantes et ne pas créer de page FEATURES dédiée
|
||||||
|
|
||||||
|
Ventiler les infos de fixknowledge dans les pages wiki existantes et ne pas créer de page FIXKNOWLEDGE dédiée
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
29
.cursor/agents/evol.md
Normal file
29
.cursor/agents/evol.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: evol
|
||||||
|
description: En charge des évolutions. Implémente les évolutions, documente les spécifications dans le wiki (docs/ puis cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh), prépare le commit puis lance push-by-script. Réponse structurée selon cloture-evolution.mdc.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent evol (évolutions)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu es l'agent evol, en charge des **évolutions** (nouvelles fonctionnalités, améliorations, refactors non correctifs).
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
## Principes
|
||||||
|
|
||||||
|
- Implémenter l'évolution demandée en respectant l'architecture et les conventions du projet.
|
||||||
|
- Documenter les spécifications dans le wiki (mettre à jour le fichier correspondant dans docs/ puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, ou éditer la page wiki concernée). **`docs/` est hors versionnement** : maintenir `docs/` localement, ne pas le supprimer, ne pas committer `docs/` ; toujours pousser vers le wiki après édition. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé de ce qu'il fait, puis l'exécuter.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Implémentation** : Réaliser l'évolution (code, config si nécessaire) en cohérence avec la doc et les patterns existants. Ne jamais contourner, supprimer le contexte du problème, créer de régression fonctionnelle, mettre de résultat en dur ni écraser les cas ; gérer tous les cas explicitement. **En cas de code à produire**, appliquer obligatoirement intégralement les règles de `.cursor/agents/code.md` (agent commande /code).
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
124
.cursor/agents/fix-lint.md
Normal file
124
.cursor/agents/fix-lint.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
name: fix-lint
|
||||||
|
description: Corriger les erreurs de lint backend, frontend et ressources partagées. À utiliser quand des erreurs de lint sont à corriger dans le monorepo.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Corriger les erreurs de lint (backend, frontend, ressources)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Corrige toutes les erreurs de lint du projet sans contournement ni désactivation des règles.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
## Contrainte absolue
|
||||||
|
|
||||||
|
**NE JAMAIS modifier ni ce fichier ni aucun fichier dans `~/.cursor/`.** Ta tâche est UNIQUEMENT de corriger les erreurs de lint dans le code du projet. Les répertoires à linter (backend, frontend, ressources partagées, etc.) sont définis par le projet : soit par convention (ex. backend, frontend, shared), soit dans `projects/<slug>.json` (clé `build_dirs` ou documentation du projet). Ne pas modifier ni améliorer la définition de cet agent.
|
||||||
|
|
||||||
|
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
|
||||||
|
|
||||||
|
## Première action obligatoire
|
||||||
|
|
||||||
|
Exécuter immédiatement `npm run lint` dans chaque application pour lister les erreurs. Ne pas modifier de fichiers avant d'avoir la liste des erreurs.
|
||||||
|
|
||||||
|
## Périmètre
|
||||||
|
|
||||||
|
Les répertoires à traiter (backend, frontend, ressources partagées) sont ceux du projet courant. Consulter `projects/<slug>.json` (clé `build_dirs`) lorsque le dépôt utilise ia_dev en submodule (slug : `.ia_project` ou `IA_PROJECT`). Sinon, suivre la structure du dépôt (ex. backend, frontend, shared ou noms définis dans la doc du projet).
|
||||||
|
|
||||||
|
## Concurrence
|
||||||
|
|
||||||
|
Ne pas lancer si un déploiement est en cours.
|
||||||
|
Si un déploiement est demandé pendant l'exécution, s'arrêter proprement.
|
||||||
|
|
||||||
|
## Processus à suivre obligatoirement
|
||||||
|
|
||||||
|
1. Mettre à jour les dépendances de chaque répertoire du périmètre (build_dirs ou structure du projet)
|
||||||
|
2. Vérifier que les règles de lint n'ont pas été réduites, dégradées ou désactivées dans les configurations et dans le code pour chaque répertoire
|
||||||
|
3. Lancer un lint fix sur chaque répertoire
|
||||||
|
4. Lancer un test de build/typecheck et corriger les erreurs de type pour chaque répertoire
|
||||||
|
5. Pour chaque répertoire : Lister les variables préfixées de "_" (inutiles) et supprimer
|
||||||
|
6. Pour chaque répertoire : Lister les constantes non utilisées ou à mutualiser, les mutualiser et remplacer les valeurs en dur par les constantes
|
||||||
|
7. Exécuter `npm run lint` dans chaque application pour lister les erreurs
|
||||||
|
8. Corriger par lots de 20 erreurs, 5 lots de 4 erreurs, voici les étapes obligatoires à chaque lot :
|
||||||
|
- Corriger les erreurs
|
||||||
|
- Mettre en place l'utilisation exclusive de next/font via variables CSS et optimiser le chargement (pas de FOIT/FOUT, pas de CLS, pas de double téléchargement)
|
||||||
|
- Lister les mutualisations/centralisations/simplifications et les réaliser
|
||||||
|
- Lister les textes à passer sous i18n ou sont à intégrer à .secrets/test/seed-site-texts-test.ts et migrer
|
||||||
|
- Lister le code mort et le supprimer
|
||||||
|
9. Lancer un lint fix sur chaque répertoire du périmètre
|
||||||
|
10. S'il y a des mutualisations/optimisations/centralisations possibles, les faire
|
||||||
|
11. Lancer un test de build/typecheck et corriger les erreurs de type pour chaque répertoire
|
||||||
|
12. Compléter la documentation selon `.cursor/agents/docupdate.md`. L'appel à l'agent docupdate est centralisé dans `.cursor/rules/cloture-evolution.mdc` (étape 12) — l'exécuter en clôture selon cette étape.
|
||||||
|
|
||||||
|
## Ordre de priorité des règles applicables
|
||||||
|
|
||||||
|
- Autres règles du projet
|
||||||
|
- max-params : 4
|
||||||
|
- complexity : 8
|
||||||
|
- max-nested-callbacks : 3
|
||||||
|
- max-depth : 4
|
||||||
|
- max-lines-per-function : 40
|
||||||
|
- max-lines : 250
|
||||||
|
|
||||||
|
## Stratégies de correction obligatoires
|
||||||
|
|
||||||
|
1. Ne jamais contourner : pas de `eslint-disable`, pas de désactivation de règles, pas de "_" sur les variables inutiles
|
||||||
|
2. **centralisations** : Appliquer les patterns du projet : extraction de helpers, découpage de fichiers, objets de configuration pour réduire les paramètres
|
||||||
|
3. **max-params** : regrouper dans un objet de configuration typé
|
||||||
|
4. **complexity** : extraire des branches dans des fonctions dédiées
|
||||||
|
5. **max-depth** : aplatir les imbrications avec early return
|
||||||
|
6. **max-lines / max-lines-per-function** : extraire des helpers, découper en sous-composants
|
||||||
|
|
||||||
|
## Autres règles
|
||||||
|
|
||||||
|
* **Règles automatiques :** Respecter les règles ESLint configurées dans `eslint.config.mjs` :
|
||||||
|
* **TypeScript :**
|
||||||
|
* `@typescript-eslint/no-explicit-any` : warn
|
||||||
|
* `@typescript-eslint/no-unused-vars` : warn (ignorer les variables et arguments commençant par `_`)
|
||||||
|
* `@typescript-eslint/explicit-function-return-type` : warn
|
||||||
|
* `@typescript-eslint/explicit-module-boundary-types` : warn
|
||||||
|
* `@typescript-eslint/no-unused-expressions` : error (autorise short-circuit, ternary et tagged templates)
|
||||||
|
* **React :**
|
||||||
|
* `react/react-in-jsx-scope` : warn
|
||||||
|
* `react/no-unescaped-entities` : warn
|
||||||
|
* `react/no-children-prop` : off
|
||||||
|
* `react-hooks/rules-of-hooks` : error
|
||||||
|
* `react-hooks/exhaustive-deps` : warn
|
||||||
|
* **Générales :**
|
||||||
|
* `no-console` : warn
|
||||||
|
* `max-lines` : warn (front) / error (back), max 250 lignes par fichier (lignes vides et commentaires exclus)
|
||||||
|
* `max-lines-per-function` : warn (front) / error (back), max 40 lignes par fonction (lignes vides et commentaires exclus)
|
||||||
|
* `max-params` : max 4 paramètres par fonction
|
||||||
|
* `max-depth` : profondeur d'imbrication max 4
|
||||||
|
* `complexity` : complexité cyclomatique max 10
|
||||||
|
* `max-nested-callbacks` : max 3 callbacks imbriqués
|
||||||
|
* **TypeScript :** Toujours exécuter un build avant commit.
|
||||||
|
* **Build :** Vérifier que le build passe sans erreurs.
|
||||||
|
* **Dépassements :** Si un fichier/fonction dépasse les limites :
|
||||||
|
1. Découper immédiatement si faisable
|
||||||
|
2. Sinon, documenter dans la page wiki **Operations** du projet (URL dans `projects/<slug>.json` → `git.wiki_url`) avec plan de refactor + échéance
|
||||||
|
3. Ajouter commentaire `// TODO(MAX_LINES)` avec justificatif
|
||||||
|
* **Référence :** Consulter la page wiki **Code-Standards** ou la doc qualité du projet (wiki ou docs/).
|
||||||
|
|
||||||
|
#### 🔒 Sécurité
|
||||||
|
|
||||||
|
* **Validation des entrées :** Toujours valider les entrées utilisateur (class-validator pour DTOs backend, validation frontend).
|
||||||
|
* **Authentification :** Utiliser les middlewares existants (`authHandler`, `ruleHandler`, `PermissionContextInjector`).
|
||||||
|
* **Secrets :** Jamais de secrets en dur. Utiliser `system_configuration` en base de données.
|
||||||
|
* **Logging sensible :** Ne jamais logger de données sensibles (RIB, tokens, OTP). Utiliser Winston uniquement.
|
||||||
|
* **Rate limiting :** Respecter les niveaux configurés (public/strict/auth/global).
|
||||||
|
* **Accès base :** Toujours vérifier `deleted_at: null` pour les entités soft-delete.
|
||||||
|
* **Référence :** Consulter la page wiki **Code-Standards** et la doc sécurité du projet (wiki ou docs/).
|
||||||
|
|
||||||
|
## Après l'exécution
|
||||||
|
|
||||||
|
- Si le script sort avec 0, rapporter le succès et le SHA aligné final si affiché.
|
||||||
|
- Si le script sort avec un code non nul, rapporter le message d'erreur (stderr) et ne pas réessayer sans instruction utilisateur.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
61
.cursor/agents/fix-search.md
Normal file
61
.cursor/agents/fix-search.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: fix-search
|
||||||
|
description: En charge des investigations en lecture seule (docs, code, configs, logs, BDD). Recherche la root cause, vérifie les hypothèses. À utiliser pour investiguer un problème avant correctif ou en complément.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent fix-search (investigations)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu es l'agent fix-search, en charge des **investigations** en vue d'identifier la cause et la root cause d'un problème remonté.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
## Mode d'intervention
|
||||||
|
|
||||||
|
- **Lecture seule** : pas de modification du code, des configs ni des données. Consultation uniquement.
|
||||||
|
- **Périmètre** : documentation (wiki du projet — URL dans `projects/<slug>.json` → `git.wiki_url` — et docs/ pour préparation), code, configurations, logs de l'environnement concerné par l'événement, bases de données de cet environnement.
|
||||||
|
- **Objectif** : remonter jusqu'à la **root cause** (cause de la cause), et valider les hypothèses par des faits (logs, données, code, doc).
|
||||||
|
|
||||||
|
## Processus obligatoire
|
||||||
|
|
||||||
|
1. **Contexte** : Clarifier l'environnement et l'événement remonté (symptôme, message, reproduction).
|
||||||
|
2. **Hypothèses** : Formuler des hypothèses de cause (chaîne cause → root cause).
|
||||||
|
3. **Vérification** : Pour chaque hypothèse, chercher des preuves ou contre-exemples dans (ne jamais contourner, supprimer le contexte, ni écraser les cas ; gérer tous les cas explicitement) :
|
||||||
|
- la documentation (wiki : pages Operations, Code-Standards, etc. — ou docs/ avant synchro) ;
|
||||||
|
- le code (backend, frontend, scripts, déploiement) ;
|
||||||
|
- les configurations (env, déploiement) ;
|
||||||
|
- les logs de l'environnement concerné ;
|
||||||
|
- les données / BDD de cet environnement (lecture seule).
|
||||||
|
4. **Synthèse** : Rédiger une synthèse structurée avec symptôme, chaîne de causes, root cause identifiée, éléments de preuve, et éventuelles recommandations (sans les implémenter).
|
||||||
|
|
||||||
|
## Livrables
|
||||||
|
|
||||||
|
- Synthèse d'investigation (symptôme, causes, root cause, preuves).
|
||||||
|
- Si des documents d'investigation ou de retour d'expérience doivent être créés ou complétés, les rédiger dans le wiki (page Operations ou autre) ou dans `docs/` puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`. **`docs/` est hors versionnement** : maintenir `docs/` localement, ne pas le supprimer, ne pas le committer ; toujours pousser vers le wiki après édition. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé, puis l'exécuter. **Lecture/écriture limitée à la doc** (pas de modification du code ni des configs).
|
||||||
|
- Préparer le commit avec le format défini dans `.cursor/agents/push-by-script.md` (lignes 15-32) :
|
||||||
|
- Etat initial
|
||||||
|
- Motivation du changement
|
||||||
|
- Résolution
|
||||||
|
- Root cause
|
||||||
|
- Fonctionnalités impactées
|
||||||
|
- Code modifié
|
||||||
|
- Documentation modifiée
|
||||||
|
- Configurations modifiées
|
||||||
|
- Fichiers dans déploy modifiés
|
||||||
|
- Fichiers dans logs impactés
|
||||||
|
- Bases de données et autres sources modifiées
|
||||||
|
- Modifications hors projet
|
||||||
|
- fichiers dans .cursor/ modifiés
|
||||||
|
- fichiers dans .secrets/ modifiés
|
||||||
|
- nouvelle sous sous version dans VERSION
|
||||||
|
- CHANGELOG.md mise à jour (oui/non)
|
||||||
|
- Lancer et **exécuter intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) avec ce message de commit. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
39
.cursor/agents/fix.md
Normal file
39
.cursor/agents/fix.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: fix
|
||||||
|
description: En charge des correctifs. Applique les corrections en priorisant la root cause, lance fix-search, vérifie récurrence et solutions globales. Documente dans le wiki (docs/ puis cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh) et prépare le commit puis push-by-script.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent fix (correctifs)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu es l'agent fix, en charge des **correctifs** à partir d'un problème remonté ou d'une investigation préalable.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
## Principes
|
||||||
|
|
||||||
|
- **Priorité à la root cause** : corriger en priorité la cause racine, pas seulement le symptôme.
|
||||||
|
- **Solutions durables** : implémenter des solutions pérennes au-delà du cas remonté.
|
||||||
|
- **Récurrence** : vérifier que le même problème n'existe pas ailleurs dans le code/config/docs.
|
||||||
|
- **Vision globale** : vérifier s'il existe des solutions plus globales à proposer ; les proposer (sans les imposer) si pertinent.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Investigation** : Lancer et **exécuter intégralement** l'agent `.cursor/agents/fix-search.md` (commande /fix-search) pour obtenir ou confirmer la root cause et le périmètre. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
|
||||||
|
|
||||||
|
2. **Corrections** :
|
||||||
|
- Corriger la root cause en priorité.
|
||||||
|
- Ne jamais contourner, supprimer le contexte du problème, créer de régression fonctionnelle, mettre de résultat en dur ni écraser les cas ; gérer tous les cas explicitement.
|
||||||
|
- Étendre la correction aux endroits similaires identifiés.
|
||||||
|
- Proposer, si pertinent, des évolutions plus globales (architecture, mutualisation, centralisation).
|
||||||
|
- **Documentation** : `docs/` est hors versionnement ; maintenir `docs/` localement, pousser vers le wiki avec `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, ne pas committer `docs/`.
|
||||||
|
- **En cas de code à produire**, appliquer intégralement les règles de `.cursor/agents/code.md` (agent commande /code).
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
80
.cursor/agents/gitea-issues-process.md
Normal file
80
.cursor/agents/gitea-issues-process.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: gitea-issues-process
|
||||||
|
description: Traite les tickets Gitea (issues) en s'appuyant au maximum sur les scripts gitea-issues/. Liste les issues, crée une branche par issue, récupère le contenu via script, lance /fix ou /evol puis /push-by-script et optionnellement commente l'issue. Push direct uniquement ; ne jamais créer de pull request.
|
||||||
|
model: inherit
|
||||||
|
is_background: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent gitea-issues-process
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev (scripts gitea-issues, spooler) est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
**À lire en début d'exécution** (documentation fournie à l'agent) :
|
||||||
|
- projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md (contexte d'exécution, scripts)
|
||||||
|
- .cursor/agents/agent-loop.md (fichier témoin, variables, boucles)
|
||||||
|
- projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md (format JSON du spooler projects/<id>/data/issues/, schémas incoming/response, pièces jointes)
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` ; `<id>` = contenu du fichier `../ai_project_id` (à la racine du dépôt projet, parent de ia_dev). Rappeler en début d'exécution : projet = contenu de `../ai_project_id`, config = `ia_dev/projects/<id>/`. Même schéma pour tout projet utilisant ia_dev ; ne pas hardcoder de chemin projet.
|
||||||
|
|
||||||
|
Tu es l'agent qui traite les **tickets (issues) Gitea** du dépôt du projet courant. Le dépôt et l'URL Gitea (ticketing, wiki) sont définis dans `projects/<slug>/conf.json` (clé `tickets.ticketing_url`, `git.wiki_url`) ; le slug projet est donné par `.ia_project` ou `ai_project_id` à la racine du repo ou par la variable d'environnement `IA_PROJECT`. Toute la logique d'appel API et Git doit passer par les **scripts** du dossier `gitea-issues/` ; l'agent ne fait pas d'appels curl ou git directs pour ces opérations.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : au début et à la fin d'exécution, afficher date/heure, **projet** (contenu de `../ai_project_id`), **branche** et **répertoire de travail** du dépôt dans `../` (pas ceux de `ia_dev`).
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (chaque script `gitea-issues/*.sh` avant de l'appeler).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options/arguments, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- `GITEA_TOKEN` défini ou fichier `.secrets/gitea-issues/token` présent.
|
||||||
|
- Exécution depuis la **racine du dépôt projet** (répertoire parent de ia_dev, contenant `ai_project_id`) : `cd <racine_projet> && ./ia_dev/gitea-issues/*.sh`. Depuis le workspace ia_dev, racine projet = `..`. Ne pas hardcoder le chemin d'un projet ; fonctionne pour tout projet configuré par `../ai_project_id`.
|
||||||
|
- Dépendances : `jq`, `curl` (les scripts les utilisent).
|
||||||
|
|
||||||
|
**Contexte** : `gitea-issues/` est dans ia_dev ; le projet est identifié par `../ai_project_id` ; la config est dans `ia_dev/projects/<id>/`. `.secrets` est sous ia_dev (`./.secrets`), indépendant du projet parent. Voir `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (Contexte d'exécution).
|
||||||
|
|
||||||
|
## Workflow (script au maximum)
|
||||||
|
|
||||||
|
1. **Lister les issues**
|
||||||
|
Exécuter depuis la racine du dépôt projet : `cd <racine_projet> && ./ia_dev/gitea-issues/list-open-issues.sh --lines` (depuis workspace ia_dev : `cd .. && ./ia_dev/gitea-issues/list-open-issues.sh --lines`). Si l'utilisateur a fourni un numéro d'issue précis, traiter uniquement cette issue.
|
||||||
|
|
||||||
|
2. **Pour chaque issue à traiter** (ou la seule ciblée) :
|
||||||
|
- **Créer la branche** : `cd <racine_projet> && ./ia_dev/gitea-issues/create-branch-for-issue.sh <issue_number> [base]` (base par défaut : `test`). Ne pas inventer de commande git ; utiliser uniquement ce script.
|
||||||
|
- **Récupérer le contenu du ticket** : `cd <racine_projet> && ./ia_dev/gitea-issues/print-issue-prompt.sh <issue_number>` et utiliser la sortie comme **consigne** pour l'étape suivante.
|
||||||
|
- **Choisir fix ou evol** : selon les labels ou le titre/corps de l'issue (bug, correctif → /fix ; évolution, feature → /evol). En cas de doute, privilégier /evol.
|
||||||
|
- **Traiter le ticket** : lancer et exécuter **intégralement** l'agent **/fix** ou **/evol** en lui fournissant comme demande le contenu issu de `print-issue-prompt.sh` (titre + corps de l'issue).
|
||||||
|
- **Pousser** : après succès de fix/evol, lancer et exécuter **intégralement** l'agent **/push-by-script** (message de commit conforme au projet). Push direct sur la branche ; ne jamais créer de pull request.
|
||||||
|
- **Commenter l'issue (optionnel)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/comment-issue.sh <issue_number> "Traitement effectué dans la branche issue/<num>. Commit poussé."` (ou message adapté).
|
||||||
|
|
||||||
|
3. **Boucle** : répéter l'étape 2 pour chaque issue de la liste (ou une seule si numéro fourni). Ne pas traiter en parallèle : une issue après l'autre.
|
||||||
|
|
||||||
|
## Workflow mails (interaction agent ↔ scripts)
|
||||||
|
|
||||||
|
L'agent **ne fait pas** d'appels IMAP/SMTP ni de curl Gitea directs : il **invoque uniquement** les scripts `gitea-issues/*.sh` depuis la racine du dépôt projet. Les scripts lisent la config (`.secrets` sous ia_dev) et font les appels réels.
|
||||||
|
|
||||||
|
**Ordre pour traiter les mails en attente** : deux sources possibles. **Aucun enregistrement ne doit être supprimé.**
|
||||||
|
|
||||||
|
**A. Spooler data/issues (prioritaire)** — Un seul fichier par message (`<base>.pending`). Le statut est dans le JSON (`status`: `pending` | `responded`). À traiter = fichiers dont `status` est `pending`.
|
||||||
|
|
||||||
|
- **Lister les mails à traiter** : exécuter `./ia_dev/gitea-issues/list-pending-spooler.sh`. Sortie = chemins des fichiers `projects/<id>/data/issues/<date>.<id>.<from>.pending` pour lesquels `status == "pending"`. Ne traiter **que** ces fichiers.
|
||||||
|
- **Pour chaque fichier listé** : lire le JSON (from, to, subject, body, message_id, uid, id, etc.). Le **base** est le nom du fichier sans `.pending` (ex. `2026-03-14T094530.a1b2c3d4.user_example.com`). Répondre uniquement si pertinent (demande d'info, évolution, etc.). Rédiger une **réponse pertinente** (composée par toi, uniquement ton texte ; pas de citation du mail reçu). Appeler `mail-send-reply.sh --to <from> --subject "Re: ..." --body "<ta réponse>" --in-reply-to "<message_id du JSON>"`. **Ne pas appeler** `mail-mark-read.sh` (inutile avec le spooler). Après envoi réussi : appeler `./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject "Re: ..." --body "<ta réponse>" --in-reply-to "<message_id>"`. Le script met à jour le **même** fichier (ajout de `response`, `status` = `responded`). Optionnel : `mail-thread-log.sh append-sent` pour tracer.
|
||||||
|
|
||||||
|
**B. Legacy agent-loop.pending** — Mails « non lus » listés par `mail-list-unread.sh` (également limités à partir du 10 mars 2026 / `MAIL_SINCE_DATE`).
|
||||||
|
|
||||||
|
- **Lister** : exécuter `./ia_dev/gitea-issues/mail-list-unread.sh`. Pour chaque UID listé : `mail-get-thread.sh <uid>`, `mail-thread-log.sh init --uid <uid>`, rédiger réponse, `mail-send-reply.sh`, **puis** `mail-mark-read.sh <uid>` uniquement après succès de l'envoi, puis `mail-thread-log.sh append-sent`.
|
||||||
|
|
||||||
|
**Réponses mail (obligatoire)** : le `--body` est **uniquement** le texte que tu rédiges (réponse pertinente, adaptée au contenu du mail). Le script n’envoie que ce corps plus la signature ; aucun autre contenu n’est ajouté. Ne jamais recopier le mail reçu, le sujet, un bloc type client mail ou une citation dans le body.
|
||||||
|
|
||||||
|
**Récupération mails (spooler data/issues)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/tickets-fetch-inbox.sh` pour récupérer les mails filtrés par `tickets.authorized_emails` (conf.json) et les écrire en `projects/<id>/data/issues/*.pending` (JSON). Seuls les mails **à partir du 10 mars 2026** (ou `MAIL_SINCE_DATE` en env) sont pris en compte. Pas de marquage lu/non lu. **Boucle legacy (non lu)** : si l'utilisateur demande « Lance la boucle récupération emails… N itérations », exécuter `./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]`. Mails en attente : **projects/<id>/data/issues/*.pending** (prioritaire) ou `projects/<id>/logs/gitea-issues/agent-loop.pending` ; les traiter selon le workflow ci-dessus.
|
||||||
|
|
||||||
|
**Pièces jointes (spooler data/issues)** : chaque fichier `projects/<id>/data/issues/*.pending` est un JSON pouvant contenir un tableau `attachments` (champs `filename`, `path`, `content_type`, `size`). Les fichiers sont stockés sous `projects/<id>/data/issues/<base>.d/` (`<base>` = `<date>.<id>.<from>`) ; `path` est relatif à `data/issues/`. Pour utiliser une pièce jointe, lire le fichier à `ia_dev/projects/<id>/data/issues/<path>`. Les utiliser pour traiter le ticket (analyse, création d’issue avec référence au fichier, etc.) sans les supprimer.
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- Ne pas appeler l'API Gitea ni exécuter des commandes git pour les issues en dehors des scripts `gitea-issues/*.sh`.
|
||||||
|
- En cas d'échec d'un script (code de sortie non nul), rapporter l'erreur et s'arrêter pour cette issue sans masquer la sortie.
|
||||||
|
- Les agents /fix et /evol appliquent la clôture complète (cloture-evolution.mdc) ; ne pas court-circuiter leur workflow.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc` en fin de réponse (tous les points, y compris sub-agents par projet, docupdate, etc.).
|
||||||
66
.cursor/agents/notary-ai-loop.md
Normal file
66
.cursor/agents/notary-ai-loop.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: notary-ai-loop
|
||||||
|
description: Orchestre la boucle de traitement des questions IA notaire (spooler pending). Liste les pending, lance notary-ai-process pour chaque lot. Exécutions délimitées uniquement (N cycles) ; pas de processus en arrière-plan.
|
||||||
|
model: inherit
|
||||||
|
is_background: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent notary-ai-loop
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/`. L'**id** est résolu par **AI_AGENT_TOKEN**, **IA_PROJECT**, **ai_project_id** ou **.ia_project** (voir `projects/README.md`). Racine du dépôt = parent de ia_dev. Scripts : `ia_dev/ai_working_help/notary-ai/`.
|
||||||
|
|
||||||
|
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, **projet** (slug), **branche** et **répertoire de travail** du dépôt (parent de ia_dev).
|
||||||
|
|
||||||
|
Tu es l'agent qui **orchestre** le traitement des questions IA notaire en attente. Tu ne produis pas les réponses toi‑même : le traitement (lecture du pending, production des 4 champs, écriture responded) est fait par l'**agent notary-ai-process**. Tu lances les scripts et le sous-agent selon la demande.
|
||||||
|
|
||||||
|
**Règle** : ne **jamais** lancer de boucle infinie en arrière-plan (nohup / &). Gérer uniquement des **exécutions délimitées** (x cycles ou une fois).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Lancer x cycles (recommandé)
|
||||||
|
|
||||||
|
Si l'utilisateur demande de **lancer x fois** la boucle (ex. « 3 cycles », « lance la boucle 5 fois ») :
|
||||||
|
|
||||||
|
Pour chaque cycle `i` de 1 à x :
|
||||||
|
|
||||||
|
1. **Lister les pending**
|
||||||
|
Exécuter depuis la racine du dépôt (depuis ia_dev : `cd ..`) :
|
||||||
|
`cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`
|
||||||
|
Sortie : un chemin par ligne (fichiers dans `projects/<slug>/data/notary-ai/pending/`).
|
||||||
|
|
||||||
|
2. **Traitement une fois**
|
||||||
|
Si la sortie est **non vide** : lancer **intégralement** l'agent **notary-ai-process** avec un prompt du type :
|
||||||
|
« Traite les questions IA notaire en attente : exécute `./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh` puis pour chaque fichier listé lis le JSON (request_uid, question, folder_context), produis les 4 champs (answer, nextActionsTable, membersInfoSheet, synthesisRecommendation) et appelle `write-response-notary-ai.sh --request-uid <uid> --answer "..." --next-actions-table "..." --members-info-sheet "..." --synthesis-recommendation "..."`. »
|
||||||
|
Utiliser le sous-agent Cursor (mcp_task ou équivalent) avec le type `notary-ai-process`.
|
||||||
|
Si la sortie est **vide**, ne pas lancer l'agent ; passer à l'étape 3.
|
||||||
|
|
||||||
|
3. **Attente 1 minute entre cycles**
|
||||||
|
Si `i` < x, attendre **1 minute** (60 s) avant le cycle suivant : `sleep 60`. Pas d'attente après le dernier cycle.
|
||||||
|
|
||||||
|
Répéter les étapes 1 à 3 pour les x cycles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Traiter une seule fois
|
||||||
|
|
||||||
|
Si l'utilisateur demande de **traiter une fois** les questions en attente (sans boucle) :
|
||||||
|
|
||||||
|
- Exécuter `cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`.
|
||||||
|
- Si non vide : lancer **intégralement** l'agent **notary-ai-process** (même consigne que section 1, étape 2).
|
||||||
|
- Si vide : indiquer qu'il n'y a rien à traiter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Autres demandes
|
||||||
|
|
||||||
|
- **Consulter les pending** : exécuter `cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh` et afficher les chemins (ou le contenu d’un fichier pour vérification).
|
||||||
|
- **Documentation** : `ia_dev/ai_working_help/docs/notary-ai-api.md` (API, spooler, scripts). Agent de traitement : `.cursor/agents/notary-ai-process.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- Pas de processus en arrière-plan ; boucles par exécutions délimitées uniquement.
|
||||||
|
- Répertoire d'exécution des scripts : **racine du dépôt** (parent de ia_dev). Depuis le workspace ia_dev, utiliser `cd ..` avant d’invoquer les scripts.
|
||||||
|
- Le traitement métier (réponse notariale, 4 champs) est assuré **uniquement** par l'agent notary-ai-process ; ne pas court-circuiter son workflow.
|
||||||
|
- Ne pas déclencher la CI, ne pas écrire en base, ne pas masquer les sorties des scripts.
|
||||||
59
.cursor/agents/notary-ai-process.md
Normal file
59
.cursor/agents/notary-ai-process.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: notary-ai-process
|
||||||
|
description: Traite les questions IA notaire en attente (spooler pending). Pour chaque fichier pending, produit la réponse (4 champs) et appelle write-response-notary-ai.sh. À lancer manuellement ou par notary-ai-loop.
|
||||||
|
model: inherit
|
||||||
|
is_background: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent notary-ai-process
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/`. L'**id** est résolu par **AI_AGENT_TOKEN** (token des requêtes API), **IA_PROJECT**, **ai_project_id** ou **.ia_project** à la racine du dépôt parent. Racine du dépôt = parent de ia_dev. Les scripts notary-ai sont dans `ia_dev/ai_working_help/notary-ai/`.
|
||||||
|
|
||||||
|
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, **projet** (slug), **branche** et **répertoire de travail** du dépôt (parent de ia_dev).
|
||||||
|
|
||||||
|
Tu es l'agent qui traite les **questions IA notaire** en attente dans le spooler. Tu ne reçois pas les requêtes directement : l'application métier (ex. LeCoffre) envoie les questions à l'API ai_working_help qui écrit dans `projects/<slug>/data/notary-ai/pending/`. Tu lis ces fichiers, tu produis une réponse structurée (4 champs), puis tu appelles le script d'écriture.
|
||||||
|
|
||||||
|
## Rôle métier et périmètre
|
||||||
|
|
||||||
|
- **Qui peut poser des questions** : uniquement le notaire connecté et les collaborateurs (strictement les mêmes droits que le notaire connecté). Les invités et les tiers ne peuvent pas utiliser ce chat.
|
||||||
|
- **Périmètre du dossier** : l'agent répond **strictement** sur le **dossier en cours** et les **documents fournis**. Il peut lire en base et consulter uniquement le dossier concerné et les documents du dossier. Aucun autre dossier, aucun accès externe.
|
||||||
|
- **Spécialisation** : droit, et plus encore les activités notariales. Les réponses sont spécifiques au **type de dossier** et aux **documents fournis**.
|
||||||
|
- **Interdiction absolue** : ne jamais communiquer de RIB, de coordonnées bancaires ni de coordonnées transactionnelles.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Exécution depuis la **racine du dépôt** (parent de ia_dev) : `cd <racine_projet> && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`, etc.
|
||||||
|
- **jq** installé (les scripts l'utilisent).
|
||||||
|
- Id projet résolu par `AI_AGENT_TOKEN`, `IA_PROJECT`, `ai_project_id` ou `.ia_project` (voir `projects/README.md`).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Lister les pending**
|
||||||
|
Exécuter : `cd <racine_projet> && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`
|
||||||
|
Sortie : un chemin par ligne (fichiers JSON dans `projects/<slug>/data/notary-ai/pending/`). Si vide, ne rien faire.
|
||||||
|
|
||||||
|
2. **Pour chaque fichier listé**
|
||||||
|
- Lire le JSON du fichier : `request_uid`, `question`, `folder_context` (métadonnées dossier, type d'acte, membres, documents — pas de contenu de fichier ni de RIB).
|
||||||
|
- Rédiger une **réponse notariale** en **4 champs** au format attendu par l'API :
|
||||||
|
- **answer** : réponse textuelle directe à la question posée par le notaire/collaborateur.
|
||||||
|
- **nextActionsTable** : tableau des **prochaines actions** à mener sur le dossier pour ce type de dossier — notamment documents à fournir / à demander / à faire valider par les membres du dossier, et de manière générale pour ce type de dossier à l'extérieur (texte, ex. markdown).
|
||||||
|
- **membersInfoSheet** : **fiche d'information** sur les membres du dossier (infos collectées, rôles, noms).
|
||||||
|
- **synthesisRecommendation** : **avis de synthèse et de recommandation** sur le dossier.
|
||||||
|
- Appeler le script d'écriture :
|
||||||
|
`./ia_dev/ai_working_help/notary-ai/write-response-notary-ai.sh --request-uid <request_uid> --answer "..." --next-actions-table "..." --members-info-sheet "..." --synthesis-recommendation "..."`
|
||||||
|
(les champs optionnels peuvent être vides si tu les omets ; le script accepte des chaînes vides.)
|
||||||
|
|
||||||
|
3. **Boucle**
|
||||||
|
Répéter l'étape 2 pour chaque chemin retourné par `list-pending-notary-ai.sh`. Traiter un fichier à la fois.
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- **Pas de RIB, pas de coordonnées transactionnelles** : le contexte envoyé par l'application ne contient pas de RIB ; ne jamais en inventer ni en retourner. Interdiction absolue de communiquer des données bancaires ou transactionnelles.
|
||||||
|
- **Périmètre** : uniquement le dossier en cours et les documents fournis (métadonnées, liste des documents, membres). Pas d'accès à d'autres dossiers ni à des fichiers hors projet.
|
||||||
|
- **Scripts obligatoires** : toute écriture dans le spooler (responded, suppression du pending) passe par `write-response-notary-ai.sh`. Ne pas modifier ni supprimer les fichiers à la main.
|
||||||
|
- Exécuter les scripts depuis la **racine du dépôt** (parent de ia_dev). Ne pas masquer les sorties des scripts.
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- Spooler et API : `ia_dev/ai_working_help/docs/notary-ai-api.md`
|
||||||
|
- Boucle d'orchestration : `.cursor/agents/notary-ai-loop.md`
|
||||||
72
.cursor/agents/push-by-script.md
Normal file
72
.cursor/agents/push-by-script.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: push-by-script
|
||||||
|
description: Exécute le script de push deploy/pousse.sh pour stager, committer avec un message structuré et pousser. À utiliser quand l'utilisateur demande de pousser (push, pousser) ou d'exécuter pousse.sh une fois les changements prêts.
|
||||||
|
model: inherit
|
||||||
|
is_background: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent push-by-script
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
|
||||||
|
|
||||||
|
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
Tu es l'agent push-by-script. **Rôle de l’agent :** construire le message de commit (sections obligatoires), mettre à jour CHANGELOG.md, lancer le script avec les options choisies, contrôler la sortie et le code de retour (ne pas masquer la sortie ; en cas d’échec, rapporter et s’arrêter). **Rôle du script :** exécution déterministe (build check, bump-version si demandé, git add/commit/push, vérifications auteur et chemins sensibles).
|
||||||
|
|
||||||
|
**Focus qualité et résolution de problèmes :**
|
||||||
|
- **Qualité :** Le message de commit doit contenir toutes les sections obligatoires (état initial, motivation, résolution, root cause, etc.) ; CHANGELOG.md doit être à jour. Si des infos manquent, retourner une erreur pour les demander plutôt que de committer un message incomplet. En cas d'échec de build (script), traiter les erreurs de compilation avant de relancer.
|
||||||
|
- **Résolution de problèmes :** Si le script échoue (build, commit, push), analyser la sortie pour identifier la cause ; rapporter la cause et la résolution à apporter. Ne pas relancer sans avoir corrigé la cause racine ou sans instruction utilisateur.
|
||||||
|
|
||||||
|
- **Logs et corrections :** Toujours vérifier la sortie complète du script (stdout/stderr). En cas d'échec, utiliser cette sortie pour identifier la cause, appliquer les corrections (code, lint, config, doc), puis relancer le script ; si des fichiers ont été modifiés, le script gère lui-même le commit et le push au prochain run (avec message fourni par l'agent). Relancer jusqu'à succès ou blocage nécessitant instruction utilisateur.
|
||||||
|
|
||||||
|
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
|
||||||
|
|
||||||
|
**Avant d'exécuter un script du projet :**
|
||||||
|
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/pousse.sh`).
|
||||||
|
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
|
||||||
|
3. Lancer le script uniquement après cette présentation.
|
||||||
|
|
||||||
|
**Workflow :**
|
||||||
|
|
||||||
|
1. Obtenir ou construire un message de commit au format du projet avec obligatoirement les informations :
|
||||||
|
|
||||||
|
- Etat initial
|
||||||
|
- Motivation du changement
|
||||||
|
- Résolution
|
||||||
|
- Root cause
|
||||||
|
- Fonctionnalités impactées
|
||||||
|
- Code modifié
|
||||||
|
- Documentation modifiée
|
||||||
|
- Configurations modifiées
|
||||||
|
- Fichiers dans déploy modifiés
|
||||||
|
- Fichiers dans logs impactés
|
||||||
|
- Bases de données et autres sources modifiées
|
||||||
|
- Modifications hors projet
|
||||||
|
- fichiers dans .cursor/ modifiés
|
||||||
|
- fichiers dans .secrets/ modifiés
|
||||||
|
- nouvelle sous sous version dans VERSION
|
||||||
|
- CHANGELOG.md mise à jour (oui/non)
|
||||||
|
|
||||||
|
2. Met à jour le fichier CHANGELOG.md
|
||||||
|
|
||||||
|
3. Met à jour le fichier VERSION en incrémentant une sous-sous-version : soit en appelant le script avec l'option **--bump-version** (recommandé), soit manuellement. Avec **--bump-version**, le script lit VERSION, incrémente le troisième segment (patch), réécrit le fichier ; le message de commit doit mentionner la nouvelle version si pertinent.
|
||||||
|
|
||||||
|
Pas de validation du commit à demander.
|
||||||
|
Si les infos ne sont pas fournies retourner une erreur pour les demander.
|
||||||
|
|
||||||
|
4. Exécuter depuis la racine du dépôt projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh [--bump-version]` (ou avec --remote si l'utilisateur le précise) avec le message complet sur STDIN (heredoc). Ne pas lancer `./deploy/pousse.sh` depuis un autre répertoire. Le script fait la build check, puis add/commit/push.
|
||||||
|
|
||||||
|
5. **Contrôle :** Ne pas masquer la sortie du script. Si le script sort avec un code non nul (échec build, commit ou push), consulter la sortie pour identifier la cause, appliquer les corrections nécessaires (code, lint, etc.), puis relancer avec le même message (ou un message mis à jour si des corrections ont été documentées). Si des fichiers ont été modifiés, le prochain run de pousse.sh les committera et poussera. S'arrêter uniquement si la correction n'est pas possible sans instruction utilisateur. Si le script sort avec 0, rapporter le succès.
|
||||||
|
|
||||||
|
**Contraintes :** Ne pas committer de chemins sensibles (.secrets/, .env, clés, etc.) ; le script les rejette. Git user.name doit être 4NK ou Nicolas Cantu. Ne pas utiliser --no-verify. Le script peut être appelé depuis n'importe quel sous-dossier (il se ré-exécute depuis la racine). **Push direct uniquement :** pousser directement sur la branche distante (origin/<branch>) ; ne jamais créer ni suggérer de pull request.
|
||||||
|
|
||||||
|
**Sortie :** Afficher la sortie complète du script. Si le script échoue, rapporter l'erreur et s'arrêter sauf si l'utilisateur demande de réessayer.
|
||||||
|
|
||||||
|
## Après l'exécution
|
||||||
|
|
||||||
|
- Si le script sort avec 0 : rapporter le succès.
|
||||||
|
- Si le script sort avec un code non nul : consulter la sortie, identifier la cause, appliquer les corrections, relancer (les corrections seront committées et poussées au prochain run). Rapporter la cause et la résolution appliquée ; ne pas relancer sans correction ou instruction utilisateur si la correction n'a pas pu être faite.
|
||||||
|
|
||||||
|
## Clôture complète (obligatoire, sans exception)
|
||||||
|
|
||||||
|
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.
|
||||||
10
.cursor/hooks.json
Normal file
10
.cursor/hooks.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"hooks": {
|
||||||
|
"sessionStart": [
|
||||||
|
{
|
||||||
|
"command": ".cursor/hooks/remonter-mails.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
47
.cursor/hooks/remonter-mails.sh
Executable file
47
.cursor/hooks/remonter-mails.sh
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Hook script: read projects/<id>/data/issues/*.pending (spooler tickets) and output JSON with additional_context for sessionStart.
|
||||||
|
# Run from project root (Cursor). Repo root = parent of ia_dev when script lives in ia_dev/.cursor/hooks.
|
||||||
|
# Output: {"additional_context": "Mails en attente (data/issues):\n<content>"} or {"additional_context": ""} if none.
|
||||||
|
set -euo pipefail
|
||||||
|
# Consume stdin (hook input JSON)
|
||||||
|
cat > /dev/null
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
IA_DEV="${ROOT}/ia_dev"
|
||||||
|
PROJECT_SLUG=""
|
||||||
|
[ -f "${ROOT}/ai_project_id" ] && PROJECT_SLUG="$(cat "${ROOT}/ai_project_id" | sed 's/[[:space:]]//g')"
|
||||||
|
[ -z "$PROJECT_SLUG" ] && [ -f "${ROOT}/.ia_project" ] && PROJECT_SLUG="$(cat "${ROOT}/.ia_project" | sed 's/[[:space:]]//g')"
|
||||||
|
if [ -n "$PROJECT_SLUG" ] && [ -d "${IA_DEV}/projects/${PROJECT_SLUG}" ]; then
|
||||||
|
SPOOL="${IA_DEV}/projects/${PROJECT_SLUG}/data/issues"
|
||||||
|
else
|
||||||
|
SPOOL="${ROOT}/data/issues"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$SPOOL" ]; then
|
||||||
|
printf '%s\n' "{\"additional_context\": \"\"}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTENT=""
|
||||||
|
for f in "${SPOOL}"/*.pending; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
status="$(jq -r '.status // "pending"' "$f" 2>/dev/null)"
|
||||||
|
[[ "$status" != "responded" ]] || continue
|
||||||
|
fi
|
||||||
|
CONTENT="${CONTENT}--- ${f##*/} ---"$'\n'"$(cat "$f")"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$CONTENT" ]; then
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$CONTENT" | jq -R -s '{additional_context: ("Mails en attente (data/issues):\n" + .)}'
|
||||||
|
else
|
||||||
|
ESCAPED="${CONTENT//\\/\\\\}"
|
||||||
|
ESCAPED="${ESCAPED//\"/\\\"}"
|
||||||
|
ESCAPED="${ESCAPED//$'\n'/\\n}"
|
||||||
|
printf '%s\n' "{\"additional_context\": \"Mails en attente (data/issues):\\n${ESCAPED}\"}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' "{\"additional_context\": \"\"}"
|
||||||
|
fi
|
||||||
95
.cursor/rules/cloture-evolution.mdc
Normal file
95
.cursor/rules/cloture-evolution.mdc
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
description: Règles pour tous les réponses (en fin de réponse)
|
||||||
|
alwaysApply: true
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clôture évolution / correction
|
||||||
|
|
||||||
|
- **Principe** : tout agent ou règle invoqué dans ce document ou dans un agent doit être **appliqué intégralement**, sans omission (exécuter l'agent en entier ou appliquer toutes les étapes de la règle concernée).
|
||||||
|
|
||||||
|
- Clôturer toute réponse en **appliquant intégralement** ces règles /!\ TTRES IMPORTANT ET NON NEGOCIABLE, - **Périmètre** : la clôture est **toujours complète** pour **tous les agents** — sans exception. Aucune exception : même pour les agents qui ne modifient pas le code (ex. branch-align, push-by-script), les points 2 (5 sub-agents par projet), 14 (docupdate), 16 et 17 s’appliquent. C'est toujours applicable de 1 à 19. Lister toutes les actions réaliées et non réalisées dans tous les cas de tous les points.
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin au début de chaque agent.
|
||||||
|
|
||||||
|
**Répertoire d'exécution des scripts (../) :** Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test`. Tous les scripts `deploy/` et `gitea-issues/` doivent être invoqués après `cd` vers cette racine (chemins absolus), ex. `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh`, pour ne pas dépendre du répertoire de travail courant.
|
||||||
|
|
||||||
|
**Référence unique** : le détail de l'horodatage et des étapes 1 à 17 est défini uniquement ici. Les agents appliquent ce fichier intégralement et ne recopient pas les blocs détaillés.
|
||||||
|
|
||||||
|
À la fin de toutes réponses il faut obligatoirement afficher :
|
||||||
|
|
||||||
|
1. **Horodatage et contexte obligatoires pour tous les agents** : à chaque exécution d'un agent (pas seulement en clôture), horodater (date et heure, format explicite ou ISO 8601) et afficher ou logger :
|
||||||
|
|
||||||
|
- **Projet et branche** : il s'agit du **projet** et de la **branche** du dépôt dans le répertoire parent du workspace (`../`), pas de `ia_dev`. Le **projet** est le contenu du fichier `../ai_project_id`. La **branche** et le **répertoire de travail** sont ceux de ce dépôt parent (ex. `git -C .. branch --show-current`, répertoire `..`).
|
||||||
|
- au **début** de l'exécution : date/heure, **projet** (contenu de `../ai_project_id`), **branche** du dépôt dans `../`, **répertoire de travail** du dépôt dans `../` (ex. `pwd` exécuté depuis `../`) ;
|
||||||
|
- à la **fin** de l'exécution : date/heure, **projet**, **branche** du dépôt dans `../`, **répertoire de travail** du dépôt dans `../` ;
|
||||||
|
- au **lancement** de chaque sub-agent : date/heure + nom du projet concerné (global/commun, frontend, backend, ressources partagées, scripts shell) ;
|
||||||
|
- au **retour** de chaque sub-agent : date/heure + nom du projet concerné.
|
||||||
|
|
||||||
|
2. Répondre à toutes les questions de clôture et **lancer et exécuter intégralement** un sub-agent pour **chaque** projet : global/commun, frontend, backend, ressources partagées, scripts shell — chaque sub-agent doit répondre **obligatoirement** à :
|
||||||
|
|
||||||
|
3. **Usage et création de Helpers systématique:**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
4. **Texte i18n systématique + `.secrets/<env>/env-full` (obligatoire) :**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
5. **Fallback interdits obligatoire:**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
6. **Modifications similaires à celles réalisées systématiquement :**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
7. **Optimisation / mutualisation / centralisation systématique:**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
8. **Réduction de la complexité systématique:**
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
9. **Renforcement systématique de la sécurité :**
|
||||||
|
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
10. **Code mort interdit obligatoirement:**
|
||||||
|
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
11. **Lint corrigé obligatoirement :**
|
||||||
|
|
||||||
|
- Réalisées
|
||||||
|
- Non réalisées encore
|
||||||
|
|
||||||
|
12. Lister ce qu'il reste à faire (puces)
|
||||||
|
|
||||||
|
13. Réalise le "Non réalisées encore"
|
||||||
|
|
||||||
|
14. réalise le reste à faire.
|
||||||
|
|
||||||
|
15. Si il n'a pas été lancé avant, lancer l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) **à exécuter intégralement**, avec un message de commit contenant les infos obligatoires suivantes (vérifier ou établir que ce format est en place). Voir `.cursor/agents/push-by-script.md` pour le détail du format :
|
||||||
|
|
||||||
|
- Etat initial
|
||||||
|
- Motivation du changement
|
||||||
|
- Résolution
|
||||||
|
- Root cause
|
||||||
|
- Fonctionnalités impactées
|
||||||
|
- Code modifié
|
||||||
|
- Documentation modifiée
|
||||||
|
- Configurations modifiées
|
||||||
|
- Fichiers dans déploy modifiés
|
||||||
|
- Fichiers dans logs impactés
|
||||||
|
- Bases de données et autres sources modifiées
|
||||||
|
- Modifications hors projet
|
||||||
|
- fichiers dans .cursor/ modifiés
|
||||||
|
- fichiers dans .secrets/ modifiés
|
||||||
|
- nouvelle sous sous version dans VERSION
|
||||||
|
- CHANGELOG.md mise à jour (oui/non)
|
||||||
|
|
||||||
|
16. Afficher le texte du commit.
|
||||||
72
.cursor/rules/rules.mdc
Normal file
72
.cursor/rules/rules.mdc
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
description: Règles pour tous les réponses
|
||||||
|
alwaysApply: true
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Règles pour tous aussi pour l'IA
|
||||||
|
|
||||||
|
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début et en fin d'exécution de chaque agent.
|
||||||
|
|
||||||
|
**Répertoire d'exécution des scripts (../) :** Les scripts `deploy/` et `gitea-issues/` s'exécutent depuis la **racine du dépôt projet** (répertoire contenant `ai_project_id`). Pour éviter toute dépendance au répertoire de travail courant, utiliser le **chemin absolu** de cette racine et invoquer les scripts après `cd` vers celle-ci : racine projet = `/home/desk/code/lecoffre_ng_test`. Exemple : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh`. Ne pas appeler `./deploy/...` ni `./gitea-issues/...` depuis un autre répertoire.
|
||||||
|
|
||||||
|
## Communication et langues
|
||||||
|
|
||||||
|
* Répond en français
|
||||||
|
* Code, documente le code, et fait les commits en anglais
|
||||||
|
|
||||||
|
## Restrictions et interdictions
|
||||||
|
|
||||||
|
* ne déclanche jamais la CI
|
||||||
|
* n'écris pas en base, jamais, les scripts doivent le faire
|
||||||
|
* ne masque pas les sorties des scripts
|
||||||
|
* ne fait jamais de certificats auto-signés
|
||||||
|
* ne modifie jamais les variables d'environnement
|
||||||
|
* ne configure jamais d'alternative htttp au lieu de https
|
||||||
|
* ne déploie jamais de génération de certificats sans faire valider
|
||||||
|
|
||||||
|
## Cursor, IA
|
||||||
|
|
||||||
|
* les agents doivent obligatoirement suivre toutes leurs consignes et étapes sans exception
|
||||||
|
* les agents doivent obligatoirement suivre toutes les consignes et étapes décrites dans les règles sans exception
|
||||||
|
|
||||||
|
## Tes réponses doivent obligatoirement respecter:
|
||||||
|
|
||||||
|
- Si c'est du lint toujours utiliser et **appliquer intégralement** `.cursor/agents/fix-lint.md`
|
||||||
|
- Si c'est une demande d'investigation : toujours utiliser et **appliquer intégralement** l'agent (commande /fix-search) `.cursor/agents/fix-search.md`.
|
||||||
|
- Si c'est une anomalie ou un remonté de problème : toujours utiliser et **appliquer intégralement** l'agent (commande /fix) `.cursor/agents/fix.md`.
|
||||||
|
- Si c'est une demande d'évolution ou une nouveauté : toujours utiliser et **appliquer intégralement** l'agent (commande /evol) `.cursor/agents/evol.md`.
|
||||||
|
- Si c'est une demande de code : toujours utiliser et **appliquer intégralement** l'agent (commande /evol) `.cursor/agents/code.md`.
|
||||||
|
- Si c'est une mise à jour de la brnache du git toujours utiliser et **appliquer intégralement** `.cursor/agents/push-by-script.md`.
|
||||||
|
- Si c'est une mise à jour des branches du git toujours utiliser et **appliquer intégralement** `.cursor/agents/branch-align-by-script-test.md`.
|
||||||
|
- - Si c'est un déploiement toujours utiliser et **appliquer intégralement** `.cursor/agents/deploy-by-script.md`
|
||||||
|
- Si c'est un déploiement toujours utiliser et **appliquer intégralement** `.cursor/agents/deploy-by-script.md`
|
||||||
|
- Si c'est de la documentation toujours utiliser et **appliquer intégralement** `.cursor/agents/docupdate.md`
|
||||||
|
- Toujours utiliser et **appliquer intégralement** les règles de `.cursor/rules/cloture-evolution.mdc` pour tous les agents.
|
||||||
|
- Si un agent ou une règle remonte une **erreur** ou une **optimisation** : la traiter obligatoirement (corriger ou mettre en œuvre), puis **relancer** l'agent ou la règle concerné(e) jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
|
||||||
|
- réponds en priorité aux questions posées
|
||||||
|
- ne contourne jamais le problème
|
||||||
|
- pour **tous les agents** : au début et à la fin de toute exécution, **horodater** (date et heure) et afficher le **projet** (contenu du fichier `../ai_project_id`), la **branche** et le **répertoire de travail** du dépôt dans le répertoire parent `../` (pas ceux de `ia_dev`) ;
|
||||||
|
- Clôturer toute réponse en **appliquant intégralement** `.cursor/rules/cloture-evolution.mdc` /!\ TTRES IMPORTANT ET NON NEGOCIABLE, - **Périmètre** : la clôture est **toujours complète** pour **tous les agents** — sans exception. Aucune exception : même pour les agents qui ne modifient pas le code (ex. branch-align, push-by-script), les points 2 (5 sub-agents par projet), 14 (docupdate), 16 et 17 s’appliquent. C'est toujours applicable de 1 à 19. Lister toutes les actions réaliées et non réalisées dans tous les cas de tous les points.
|
||||||
|
|
||||||
|
## Gestion de projet
|
||||||
|
|
||||||
|
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
|
||||||
|
* **Planning :** Ne fait pas de roadmap.
|
||||||
|
|
||||||
|
## Collaboration et Workflow
|
||||||
|
|
||||||
|
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
|
||||||
|
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
|
||||||
|
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
|
||||||
|
* **Résultats :** Ne présume pas de résultats non testés ou vérifiés.
|
||||||
|
* **Rapports :** Ne fait pas de rapports apres tes actions autre que celui de `.cursor/rules/cloture-evolution.mdc`
|
||||||
|
|
||||||
|
## Gestion des Fichiers
|
||||||
|
|
||||||
|
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
|
||||||
|
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
|
||||||
|
|
||||||
|
## Mise à jour de ces règles
|
||||||
|
|
||||||
|
* **Lecture seule sur .cursor:** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des modifications.
|
||||||
40
.editorconfig
Normal file
40
.editorconfig
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# TypeScript/JavaScript files
|
||||||
|
[*.{ts,tsx,js,jsx}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# JSON files
|
||||||
|
[*.json]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# YAML files
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
[*.sh]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Prisma schema
|
||||||
|
[*.prisma]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
41
.gitattributes
vendored
Normal file
41
.gitattributes
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Force LF line endings for all text files
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Explicitly declare text files you want to always be normalized and converted
|
||||||
|
# to LF line endings on checkout.
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.bash text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.sql text eol=lf
|
||||||
|
*.prisma text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.xml text eol=lf
|
||||||
|
*.mjs text eol=lf
|
||||||
|
*.cjs text eol=lf
|
||||||
|
*.config.js text eol=lf
|
||||||
|
*.config.ts text eol=lf
|
||||||
|
|
||||||
|
# Denote all files that are truly binary and should not be modified.
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
|
*.tar binary
|
||||||
|
*.gz binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Environnement
|
||||||
|
**/.env.test
|
||||||
|
**/.env.pprod
|
||||||
|
**/.env.prod
|
||||||
|
**/.env.deploy
|
||||||
|
**/.env.demo
|
||||||
|
**/.env
|
||||||
|
|
||||||
|
# Dumps BDD (nouveau chemin: .secrets/<env>/bdd.<env>)
|
||||||
|
.secrets/*/bdd.*
|
||||||
|
|
||||||
|
# Backups et certificats (nouveaux chemins: backup/bdd/, backup/certificats/, backup/nginx/)
|
||||||
|
backup/bdd/backups-local*
|
||||||
|
backup/certificats/certificats-local*
|
||||||
|
backup/nginx/*
|
||||||
|
**/*certbot/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
**/*node_modules/
|
||||||
|
# package-lock.json must be versioned for npm ci to work reliably
|
||||||
|
# **/*package-lock.json
|
||||||
|
**/*dist/
|
||||||
|
**/generated/
|
||||||
|
**/*build/
|
||||||
|
**/*coverage/
|
||||||
|
**/*.next/
|
||||||
|
**/*.npm-debug.log*
|
||||||
|
**/*.yarn-debug.log*
|
||||||
|
**/*.yarn-error.log*
|
||||||
|
**/*..pnpm-debug.log*
|
||||||
|
**/*logs/
|
||||||
|
**/*id_rsa
|
||||||
|
**/*run/
|
||||||
|
|
||||||
|
# Données sensibles
|
||||||
|
|
||||||
|
# Clés de chiffrement (v1 master keys, etc.)
|
||||||
|
.secrets/
|
||||||
|
*.master-key.txt
|
||||||
|
|
||||||
|
# Exception : migrations Prisma doivent être versionnées
|
||||||
|
!lecoffre-back-main/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
# Python
|
||||||
|
**/__pycache__/
|
||||||
|
|
||||||
|
# Fichiers temporaires
|
||||||
|
*-old
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
|
Untitled
|
||||||
|
tmp/
|
||||||
|
**/tmp/
|
||||||
|
|
||||||
|
# Full env files pour injection BDD (nouveau chemin: .secrets/<env>/env-full-<env>-for-bdd-injection.txt)
|
||||||
|
.secrets/*/env-full-*-for-bdd-injection.txt
|
||||||
|
deploy/env-full-*-for-bdd-injection.txt
|
||||||
|
|
||||||
|
**/*.vscode
|
||||||
|
lecoffre-anchor-api/test-api-ok.sh
|
||||||
|
# .env files (nouveau chemin: .secrets/<env>/.env.<env>)
|
||||||
|
.secrets/*/.env.*
|
||||||
|
.cursor/ssh_config
|
||||||
|
|
||||||
|
tmp_commit_msg.txt
|
||||||
|
|
||||||
|
# Import V1 last successful date (runtime)
|
||||||
|
deploy/import-v1-last-ok.txt
|
||||||
|
|
||||||
|
# Documentation : copie de travail pour le wiki, non versionnée
|
||||||
|
**/*.secrets/**
|
||||||
|
**/*.docs/**
|
||||||
47
.gitmessage
Normal file
47
.gitmessage
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# <type>: <subject>
|
||||||
|
#
|
||||||
|
# Brief description of the change (50 chars max recommended)
|
||||||
|
#
|
||||||
|
# Author: 4NK or Nicolas Cantu only. Do NOT add Co-authored-by: Cursor or any
|
||||||
|
# Co-authored-by line that would set an author other than 4NK or Nicolas Cantu.
|
||||||
|
|
||||||
|
**Motivations :**
|
||||||
|
* Why this change is needed
|
||||||
|
* What problem does it solve
|
||||||
|
|
||||||
|
**Root causes :**
|
||||||
|
* What is the underlying cause of the issue (if fixing a bug)
|
||||||
|
* N/A if this is a feature
|
||||||
|
|
||||||
|
**Correctifs :**
|
||||||
|
* What was fixed
|
||||||
|
* How it was fixed
|
||||||
|
|
||||||
|
**Evolutions :**
|
||||||
|
* What new features or improvements were added
|
||||||
|
* None if this is only a bug fix
|
||||||
|
|
||||||
|
**Page affectées :**
|
||||||
|
* List of affected pages/routes/components/APIs
|
||||||
|
* Use bullet points for each
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# fix: resolve authentication issue
|
||||||
|
#
|
||||||
|
# **Motivations :**
|
||||||
|
# * Users cannot login after password change
|
||||||
|
#
|
||||||
|
# **Root causes :**
|
||||||
|
# * Password hash comparison was using wrong algorithm
|
||||||
|
#
|
||||||
|
# **Correctifs :**
|
||||||
|
# * Updated password comparison to use bcrypt.compare correctly
|
||||||
|
# * Added proper error handling
|
||||||
|
#
|
||||||
|
# **Evolutions :**
|
||||||
|
# * None
|
||||||
|
#
|
||||||
|
# **Page affectées :**
|
||||||
|
# * /api/auth/login
|
||||||
|
# * /login page
|
||||||
|
#
|
||||||
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"default": false,
|
||||||
|
"MD032": true,
|
||||||
|
"MD033": true,
|
||||||
|
"MD040": true
|
||||||
|
}
|
||||||
6
.markdownlintignore
Normal file
6
.markdownlintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
lecoffre-front-main/.next
|
||||||
|
lecoffre-front-main/out
|
||||||
|
lecoffre-back-main/dist
|
||||||
|
lecoffre-ressources-dev/dist
|
||||||
56
.prettierignore
Normal file
56
.prettierignore
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sql
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.dump
|
||||||
|
*.custom
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
CHANGELOG.md
|
||||||
|
*.pdf
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
prisma/migrations/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
lecoffre-ressources-dev/dist/
|
||||||
227
CLAUDE.md
Normal file
227
CLAUDE.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
---
|
||||||
|
description: Règles pour tous aussi pour l'IA et pour Cursor
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Règles pour tous aussi pour l'IA
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
### Communication et langues
|
||||||
|
|
||||||
|
* Répond en français
|
||||||
|
* Code, documente le code, et fait les commits en anglais
|
||||||
|
|
||||||
|
### Restrictions et interdictions
|
||||||
|
|
||||||
|
* ne déclanche jamais la CI
|
||||||
|
* n'écris pas en base, jamais, les scripts doivent le faire
|
||||||
|
* ne masque pas les sorties des scripts
|
||||||
|
* ne fait jamais de certificats auto-signés
|
||||||
|
* ne modifie jamais les variables d'environnement
|
||||||
|
* ne configure jamais d'alternative htttp au lieu de https
|
||||||
|
* ne déploiement jamais de génération de certificats sans faire valider
|
||||||
|
* ne lance rien en arrière plan
|
||||||
|
|
||||||
|
### Processus de développement
|
||||||
|
|
||||||
|
* réponds en priorité aux questions posées
|
||||||
|
* avant d'implementer une solution demande la validation générales et pérennes
|
||||||
|
* ne corrige pas les bugs avant d'avoir identifier la root cause des problèmes et corrige avant tout la root cause des problèmes
|
||||||
|
* cherche la cause de la cause des problèmes jusqu'à la root cause
|
||||||
|
* il faut corriger les raisons des erreurs, cherche toujours à corriger les problèmes et ne cherche pas à les rendre acceptables
|
||||||
|
* bases toi en priorité sur des id ou des hash de id plutot que sur des libellés ou valeurs
|
||||||
|
|
||||||
|
### Vérifications et qualité
|
||||||
|
|
||||||
|
* vérifie les fichiers après modification ou lecture pour :
|
||||||
|
* ajouter des logs
|
||||||
|
* supprimer du code mort
|
||||||
|
* ajouter des commentaires ou des questions en commentaires
|
||||||
|
* corrige les erreurs de lint par 10 en lançant à chaque fois au début et à la fin des série de 10 un test de turbopack jusqu'au passage OK de turbopack
|
||||||
|
* Dans les fichiers markdown respecter MD032 (blanks-around-lists), MD033 (no-inline-html), MD040 (fenced-code-language). Exécuter `npm run lint:markdown` pour vérifier.
|
||||||
|
|
||||||
|
### Investigation et analyse
|
||||||
|
|
||||||
|
|
||||||
|
* avant d'ajouter des logs, présume de la correction à fonction des traces attendues pour vérifier en amont la cause probable, cela sur 1 ou 2 profondeur
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* a la fin des corrections met à jour la documentation générale dans docs/
|
||||||
|
* a la fin des évolutions met à jour la documentation générale dans docs/
|
||||||
|
* quand tu corrige un problème documente dans `docs/OPERATIONS.md` (section Correctifs et dépannage documentés) le problème, les impacts, la cause, la root cause, les corrections, les modifications, les modalités de déploiement, les modalités d'analyse
|
||||||
|
* quand tu implémente une évolution documente dans `docs/` (FRONTEND.md, CODE_STANDARDS.md, OPERATIONS.md selon le périmètre) l'objectif, les impacts, les modifications, les modalités de déploiement, les modalités d'analyse
|
||||||
|
|
||||||
|
## Préparation
|
||||||
|
|
||||||
|
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `deploy/`, `todoFix/`, `docs/`, `user_stories/`.
|
||||||
|
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
|
||||||
|
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todoFix/`, de `user_stories/` et le code chaque application.
|
||||||
|
* **Analyse fine :** Analyse finement `deploy/scripts/bump-version.sh`.
|
||||||
|
* **Analyse fine :** Analyse finement `deploy/scripts/build-and-deploy.sh`.
|
||||||
|
* **User Stories :** Consulter `user_stories/INDEX.md` pour comprendre les 43 user stories et leurs dépendances. Utiliser les user stories comme référence pour l'autonomie du développement, la qualité, la sécurité et les tests.
|
||||||
|
|
||||||
|
## ⚙️ Gestion de projet
|
||||||
|
|
||||||
|
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
|
||||||
|
* **Planning :** Ne fait pas de roadmap.
|
||||||
|
|
||||||
|
## 🤝 Collaboration et Workflow
|
||||||
|
|
||||||
|
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
|
||||||
|
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
|
||||||
|
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
|
||||||
|
|
||||||
|
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
|
||||||
|
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
|
||||||
|
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
|
||||||
|
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec ``**Motivations :**`,`**Root causes :**`,`**Correctifs :**`,`**Evolutions :**`,`**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||||
|
* **Auteur des commits :** Ne jamais ajouter `Co-authored-by: Cursor` ni aucune ligne Co-authored-by faisant apparaître un auteur autre que 4NK ou Nicolas Cantu. L'auteur du commit doit être 4NK ou Nicolas Cantu uniquement.
|
||||||
|
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**`, `**Root causes :**`, `**Correctifs :**`, `**Evolutions :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||||
|
* **Rapports :** Ne fait pas de rapports apres tes actions.
|
||||||
|
|
||||||
|
## ⚙️ Gestion de l'Environnement et des Configurations
|
||||||
|
|
||||||
|
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
|
||||||
|
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
|
||||||
|
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
|
||||||
|
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
|
||||||
|
|
||||||
|
## 💻 Qualité du Code et Bonnes Pratiques
|
||||||
|
|
||||||
|
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
|
||||||
|
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
|
||||||
|
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
|
||||||
|
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
|
||||||
|
|
||||||
|
#### Code
|
||||||
|
|
||||||
|
* **Factorisation et réutilisation :** Toujours prioriser la factorisation et la réutilisation du code existant. Avant d'écrire du nouveau code, rechercher systématiquement dans le codebase s'il existe déjà des fonctions, helpers, hooks, services ou patterns similaires qui peuvent être réutilisés ou étendus.
|
||||||
|
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
|
||||||
|
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
|
||||||
|
* **Patterns réutilisables :** Consulter `docs/CODE_STANDARDS.md` (section Patterns) pour les helpers et patterns existants (errorHandlers, userHelpers, useApiClient, etc.). Ne pas réinventer ce qui existe déjà.
|
||||||
|
* **Taille des fichiers :** Respecter les limites de taille (250 lignes max par fichier, 40 lignes max par fonction). max-params 4, max-depth 4, complexity 10, max-nested-callbacks 3. Documenter les exceptions dans `docs/OPERATIONS.md` (section Correctifs et dépannage) avec plan de refactor.
|
||||||
|
* **Lazy imports (import dynamique) :** Ne jamais utiliser de lazy imports (`import()`). Utiliser uniquement des imports statiques. Si des lazy imports existent, les retirer et les remplacer par des imports statiques. Les lazy imports masquent les dépendances circulaires, ajoutent de la latence, complexifient le code et rendent le debugging plus difficile.
|
||||||
|
* **Imports par défaut :** Toujours nommer les imports par défaut. Ne jamais utiliser d'imports anonymes (`import something from`). Utiliser des noms explicites pour tous les imports par défaut.
|
||||||
|
* **Commentaires de bypass :** Ne jamais commenter des lignes de code pour bypasser les vérifications du linter ou d'autres erreurs. Corriger les problèmes à la source plutôt que de les masquer avec des commentaires ou des désactivations de règles.
|
||||||
|
|
||||||
|
#### 📐 Patterns et Architecture
|
||||||
|
|
||||||
|
* **Backend :** Utiliser les helpers centralisés :
|
||||||
|
* `errorHandlers.ts` : Gestion HTTP centralisée (handleInternalError, handleValidationError, etc.)
|
||||||
|
* `errorLoggers.ts` : Logging standardisé (logError, logValidationError, etc.)
|
||||||
|
* `errorMessages.ts` : Messages d'erreur centralisés
|
||||||
|
* `userHelpers.ts` : Helpers utilisateurs (isSuperAdminUser, extractUserData, etc.)
|
||||||
|
* **Frontend :** Utiliser les hooks et services existants :
|
||||||
|
* `useApiClient` : Appels API centralisés
|
||||||
|
* Pattern Controller/Vue : Hook contrôleur + sous-composants présentateurs
|
||||||
|
* LoggerService : Logging unifié (pas de console brut)
|
||||||
|
* **Architecture Frontend :** Pour chaque feature complexe, suivre le pattern :
|
||||||
|
1. Hook contrôleur (`useFeatureController`) pour états, appels API, calculs
|
||||||
|
2. Sous-composants présentateurs pour découper l'UI
|
||||||
|
3. Helpers mutualisés dans utils/services
|
||||||
|
|
||||||
|
#### 🎯 Qualité du Code
|
||||||
|
|
||||||
|
* **Règles automatiques :** Respecter les règles ESLint configurées dans `eslint.config.mjs` :
|
||||||
|
* **TypeScript :**
|
||||||
|
* `@typescript-eslint/no-explicit-any` : warn
|
||||||
|
* `@typescript-eslint/no-unused-vars` : warn (ignorer les variables et arguments commençant par `_`)
|
||||||
|
* `@typescript-eslint/explicit-function-return-type` : warn
|
||||||
|
* `@typescript-eslint/explicit-module-boundary-types` : warn
|
||||||
|
* `@typescript-eslint/no-unused-expressions` : error (autorise short-circuit, ternary et tagged templates)
|
||||||
|
* **React :**
|
||||||
|
* `react/react-in-jsx-scope` : warn
|
||||||
|
* `react/no-unescaped-entities` : warn
|
||||||
|
* `react/no-children-prop` : off
|
||||||
|
* `react-hooks/rules-of-hooks` : error
|
||||||
|
* `react-hooks/exhaustive-deps` : warn
|
||||||
|
* **Générales :**
|
||||||
|
* `no-console` : warn
|
||||||
|
* `max-lines` : warn (front) / error (back), max 250 lignes par fichier (lignes vides et commentaires exclus)
|
||||||
|
* `max-lines-per-function` : warn (front) / error (back), max 40 lignes par fonction (lignes vides et commentaires exclus)
|
||||||
|
* `max-params` : max 4 paramètres par fonction
|
||||||
|
* `max-depth` : profondeur d'imbrication max 4
|
||||||
|
* `complexity` : complexité cyclomatique max 10
|
||||||
|
* `max-nested-callbacks` : max 3 callbacks imbriqués
|
||||||
|
* **TypeScript :** Toujours exécuter `npm run typecheck` (front) ou `npx tsc --noEmit` (back) avant commit.
|
||||||
|
* **Build :** Vérifier que `npm run build` passe sans erreurs.
|
||||||
|
* **Dépassements :** Si un fichier/fonction dépasse les limites :
|
||||||
|
1. Découper immédiatement si faisable
|
||||||
|
2. Sinon, documenter dans `docs/OPERATIONS.md` (section Correctifs et dépannage) avec plan de refactor + échéance
|
||||||
|
3. Ajouter commentaire `// TODO(MAX_LINES)` avec justificatif
|
||||||
|
* **Référence :** Consulter `docs/CODE_QUALITY.md` pour les spécifications complètes.
|
||||||
|
|
||||||
|
#### 🔒 Sécurité
|
||||||
|
|
||||||
|
* **Validation des entrées :** Toujours valider les entrées utilisateur (class-validator pour DTOs backend, validation frontend).
|
||||||
|
* **Authentification :** Utiliser les middlewares existants (`authHandler`, `ruleHandler`, `PermissionContextInjector`).
|
||||||
|
* **Secrets :** Jamais de secrets en dur. Utiliser `system_configuration` en base de données.
|
||||||
|
* **Logging sensible :** Ne jamais logger de données sensibles (RIB, tokens, OTP). Utiliser Winston uniquement.
|
||||||
|
* **Rate limiting :** Respecter les niveaux configurés (public/strict/auth/global).
|
||||||
|
* **Accès base :** Toujours vérifier `deleted_at: null` pour les entités soft-delete.
|
||||||
|
* **Référence :** Consulter `docs/CODE_SECURITY.md` pour les spécifications complètes.
|
||||||
|
|
||||||
|
#### 🧪 Tests
|
||||||
|
|
||||||
|
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
|
||||||
|
* **Outils de test disponibles :** Utiliser les outils MCP browser pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
|
||||||
|
* **Tests Browser :** Utiliser les outils MCP browser pour les tests E2E. Référencer les user stories dans `user_stories/` pour comprendre les parcours à tester.
|
||||||
|
* **User Stories comme tests :** Consulter `user_stories/INDEX.md` et les fichiers `US*.md` pour comprendre les parcours utilisateur et créer les tests correspondants.
|
||||||
|
* **Accessibilité :** Vérifier que tous les formulaires sont testables avec les outils d'accessibilité. Consulter `user_stories/ACCESSIBILITY_TESTING.md` pour les modalités de test.
|
||||||
|
* **Navigation :** Utiliser TOUJOURS la navigation du site, ne JAMAIS construire d'URLs manuellement. Suivre le parcours utilisateur naturel.
|
||||||
|
* **Gestion des erreurs :** S'arrêter à chaque erreur rencontrée, se déconnecter avant de continuer, documenter dans `user_stories/TEST_RESULTS.md`.
|
||||||
|
* **Comptes de test :** Consulter `user_stories/TEST_ACCOUNTS.md` pour les comptes disponibles. Utiliser `user_stories/scripts/prepare-test-data.sh` pour préparer les données de test.
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todoFix/` et `docs/`.
|
||||||
|
* **Structure de la documentation :**
|
||||||
|
* La documentation générale et pérenne se trouve dans `docs/`.
|
||||||
|
* Les features et corrections sont documentées dans `docs/` (OPERATIONS.md section Correctifs et dépannage, FRONTEND.md, CODE_STANDARDS.md) ; les tâches en cours dans `todoFix/`.
|
||||||
|
* Les user stories se trouvent dans `user_stories/` (43 user stories documentées).
|
||||||
|
* **User Stories :** Consulter `user_stories/INDEX.md` pour la liste complète et les dépendances. Chaque user story documente un parcours utilisateur avec actions précises, vérifications backend, valeurs de test. Utiliser comme référence pour l'autonomie du développement.
|
||||||
|
* **Qualité et sécurité :** Consulter `docs/CODE_QUALITY.md` et `docs/CODE_SECURITY.md` pour les spécifications complètes.
|
||||||
|
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
|
||||||
|
* **Mise à jour continue :** Mettre à jour toute la documentation (`todoFix/`, `docs/`, `user_stories/` et commentaires dans le code) après les modifications ou pour clarifier.
|
||||||
|
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les modifications majeures. Ce contenu est repris dans la splash notice de l'application front. Les mises à jour mineures sont ajoutées au `CHANGELOG.md` sans enlever d'élément existant.
|
||||||
|
|
||||||
|
## 📊 Logging et Gestion des Erreurs
|
||||||
|
|
||||||
|
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
|
||||||
|
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
|
||||||
|
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
|
||||||
|
|
||||||
|
## 🌐 Interactions Externes (BDD, API, Emails)
|
||||||
|
|
||||||
|
* **Base de données :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
|
||||||
|
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
|
||||||
|
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
|
||||||
|
|
||||||
|
## 🚀 Déploiement
|
||||||
|
|
||||||
|
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
|
||||||
|
* **Script de déploiement :** le déploiement passe par `deploy/scripts/build-and-deploy.sh`, ne masque pas la sortie (pas de 2>&1 par exemple).
|
||||||
|
* **Bilan de déploiement :** ne fait pas de bilan de déploiement.
|
||||||
|
* **Lancement :** ne lance aucun déploiement sans demander avant
|
||||||
|
|
||||||
|
## 🚨 Gestion des Problèmes
|
||||||
|
|
||||||
|
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
|
||||||
|
|
||||||
|
## 🗄️ Gestion des Fichiers
|
||||||
|
|
||||||
|
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
|
||||||
|
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
|
||||||
|
|
||||||
|
## Mise à jour de ces règles
|
||||||
|
|
||||||
|
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
|
||||||
|
|
||||||
|
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
|
||||||
|
|
||||||
|
## Application
|
||||||
|
|
||||||
|
* Indique l'IA que tu utilise
|
||||||
|
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# ia_dev
|
||||||
|
|
||||||
|
Dépôt de pilotage par l’IA pour les projets : **équipe d’agents IA** réutilisable par tout projet qui inclut ia_dev en submodule Git. Objectif : une équipe autonome couvrant la **documentation**, le **code** (correctifs, évolutions), le **ticketing** (issues Gitea, mails), le **devops** (push, déploiement, branches), la **sécurité** et la **qualité** (lint, règles).
|
||||||
|
|
||||||
|
**Principe** : le projet hôte n’a **aucune dépendance** vers ia_dev (aucun script du projet n’appelle ia_dev). Seul ia_dev, en fonction du paramétrage (`.ia_project`, `ai_project_id`, `projects/<id>/conf.json`), sollicite le projet (lecture de la config, appels aux scripts deploy, gitea-issues, etc.).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- **En submodule** : ce dépôt est inclus comme sous-module Git dans chaque projet. La config par projet est dans `projects/<id>/conf.json` ; le slug `<id>` est donné par le fichier `.ia_project` ou `ai_project_id` à la racine du dépôt hôte, ou par la variable d’environnement `IA_PROJECT`.
|
||||||
|
- **Scripts** : à lancer depuis la **racine du dépôt du projet** (ex. `./ia_dev/deploy/pousse.sh`, `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`).
|
||||||
|
|
||||||
|
Voir `projects/README.md` pour le schéma de configuration et les exemples.
|
||||||
|
|
||||||
|
## Agents et domaines
|
||||||
|
|
||||||
|
Les agents Cursor sont dans `.cursor/agents/`. Chaque agent indique où se trouve la doc : **projets gérés** → `projects/<id>/docs` ; **ia_dev** → `projects/ia_dev/docs`.
|
||||||
|
|
||||||
|
| Domaine | Agents / composants |
|
||||||
|
|---------|---------------------|
|
||||||
|
| **Doc** | `docupdate` ; `projects/ia_dev/docs/` (GITEA_ISSUES_SCRIPTS_AGENTS, TICKETS_SPOOL_FORMAT, WORKFLOWS_AND_COMPONENTS) ; migration wiki (`gitea-issues/wiki-migrate-docs.sh`). |
|
||||||
|
| **Code** | `fix`, `evol`, `code`, `fix-search` ; workflow correctifs/évolutions (fix-search → fix → push-by-script ; evol + code). |
|
||||||
|
| **Ticketing** | `gitea-issues-process`, `agent-loop` ; spooler `projects/<id>/data/issues` ; scripts `gitea-issues/` (issues Gitea, mails, boucle récupération/traitement) ; hook `.cursor/hooks/remonter-mails.sh`. |
|
||||||
|
| **IA notaire (ai_working_help)** | `notary-ai-loop`, `notary-ai-process` ; API `ai_working_help/server.js` (POST enqueue, GET response) ; spooler `projects/<id>/data/notary-ai/{pending,responded}` ; scripts `ai_working_help/notary-ai/` (list-pending, write-response). |
|
||||||
|
| **DevOps** | `push-by-script`, `deploy-by-script`, `deploy-pprod-or-prod`, `branch-align-by-script-from-test`, `change-to-all-branches` ; scripts `deploy/` (pousse.sh, scripts_v2, alignement branches). |
|
||||||
|
| **Sécurité / Qualité** | Règles `.cursor/rules/` ; pas de secrets en dur (`.secrets`, config par projet) ; `fix-lint` ; clôture obligatoire (`.cursor/rules/cloture-evolution.mdc`) pour tous les agents. |
|
||||||
|
|
||||||
|
Référence détaillée scripts et agents : `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md`. Index de la doc ia_dev : `projects/ia_dev/docs/README.md`.
|
||||||
|
|
||||||
|
## Répertoires d’exécution
|
||||||
|
|
||||||
|
Les scripts sont invoqués depuis la **racine du dépôt hôte**. Ils s’y placent (ou s’y ré-exécutent) avant de continuer.
|
||||||
|
|
||||||
|
- **deploy/** : `PROJECT_ROOT` = git toplevel ; ré-exécution depuis la racine si besoin. Chemin du script résolu (`readlink -f` / `realpath`) pour que `IA_DEV_ROOT` soit correct même si `deploy` est un symlink vers `ia_dev/deploy`.
|
||||||
|
- **gitea-issues/** : racine projet = parent de ia_dev ; `.secrets` sous ia_dev ; **logs** et **data** (spooler tickets) par projet sous `projects/<id>/logs/` et `projects/<id>/data/issues/` (id = slug, ex. contenu de `ai_project_id`).
|
||||||
|
- **ai_working_help/** : API (server.js) et scripts `notary-ai/` ; spooler par projet sous `projects/<id>/data/notary-ai/{pending,responded}` ; scripts invoqués depuis la racine du dépôt (parent de ia_dev). Doc : `ai_working_help/docs/notary-ai-api.md`.
|
||||||
|
|
||||||
|
## Scripts centralisés (submodule)
|
||||||
|
|
||||||
|
Les scripts suivants sont centralisés dans `ia_dev/deploy/`. Le projet n’a pas à fournir de wrapper ; on invoque depuis la racine : `./ia_dev/deploy/bump-version.sh`, `./ia_dev/deploy/pousse.sh`, etc.
|
||||||
|
|
||||||
|
- **bump-version.sh** : lecture de `projects/<id>/conf.json` (version.package_json_paths, version.splash_app_name). Invocation : `./ia_dev/deploy/bump-version.sh <version> [message]` depuis la racine du dépôt.
|
||||||
|
- **deploy-by-script-to.sh** : enchaîne change-to-all-branches (ia_dev/deploy), puis checkout/pull/deploy via `deploy/scripts_v2/deploy.sh` du projet. Le projet peut avoir `deploy/deploy-by-script-to.sh` → `exec …/ia_dev/deploy/deploy-by-script-to.sh "$@"`.
|
||||||
|
- **deploy/_lib/** : bibliothèque partagée pour les scripts de déploiement (`colors.sh`, `env-map.sh`, `ssh.sh`, `git-flow.sh`). Pour que `deploy/scripts_v2/` du projet utilise cette version : depuis la racine du projet, `rm -rf deploy/scripts_v2/_lib` puis `ln -s ../../ia_dev/deploy/_lib deploy/scripts_v2/_lib` (les scripts font `source "$SCRIPT_DIR/_lib/…"`).
|
||||||
60
ai_working_help/docs/notary-ai-api.md
Normal file
60
ai_working_help/docs/notary-ai-api.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Notary AI – API et spooler (ai_working_help)
|
||||||
|
|
||||||
|
## Rôle
|
||||||
|
|
||||||
|
**ai_working_help** est une API d’écoute qui tourne sur la **machine où Cursor et la boucle d’agents** sont utilisés (pas sur le serveur où l’app est déployée). L’application déployée appelle cette API (NOTARY_AI_AGENT_URL) pour enqueuer les questions et récupérer les réponses ; les réponses sont produites par les agents Cursor (notary-ai-loop + notary-ai-process) qui lisent/écrivent le spooler sur ce PC.
|
||||||
|
|
||||||
|
## Rôle métier et format de réponse (côté agent)
|
||||||
|
|
||||||
|
- **Utilisateurs autorisés** : strictement le notaire connecté et les collaborateurs (mêmes droits que le notaire). Les invités et les tiers ne peuvent pas utiliser le chat IA.
|
||||||
|
- **Périmètre** : l'agent répond **uniquement** sur le **dossier en cours** et les **documents fournis**. Il peut lire en base et consulter strictement le dossier concerné et les documents du dossier.
|
||||||
|
- **Spécialisation** : droit, et plus encore les activités notariales. Réponses spécifiques au type de dossier et aux documents fournis.
|
||||||
|
- **Interdiction** : ne jamais communiquer de RIB ni de coordonnées transactionnelles.
|
||||||
|
|
||||||
|
**Format de réponse (4 champs)** renvoyés par l'API dans `response` :
|
||||||
|
|
||||||
|
| Champ | Contenu |
|
||||||
|
|-------|--------|
|
||||||
|
| **answer** | Réponse textuelle directe à la question posée. |
|
||||||
|
| **nextActionsTable** | Tableau des prochaines actions à mener sur le dossier pour ce type de dossier : documents à fournir / demander / faire valider aux membres du dossier, et de manière générale pour ce type de dossier à l'extérieur. |
|
||||||
|
| **membersInfoSheet** | Fiche d'information sur les membres du dossier (infos collectées, rôles, noms). |
|
||||||
|
| **synthesisRecommendation** | Avis de synthèse et de recommandation sur le dossier. |
|
||||||
|
|
||||||
|
## Id projet et résolution
|
||||||
|
|
||||||
|
- **Côté API** : le token est trouvé en parcourant tous les projets et tous les envs comme décrit précédemment : pour chaque `projects/<id>/.secrets/<env>/ia_token`, on compare le Bearer au contenu du fichier ou à (contenu + env). La première correspondance donne l'id projet et l'env (test, pprod, prod, etc.).
|
||||||
|
- **Côté scripts** : id (et env) résolus par **MAIL_TO**, **AI_AGENT_TOKEN** (lit `.secrets/<env>/ia_token`), **IA_PROJECT**, ou **ai_project_id** / **.ia_project** à la racine du dépôt parent.
|
||||||
|
- Données par projet : `projects/<id>/data/notary-ai/` (pending/, responded/).
|
||||||
|
|
||||||
|
## API (serveur Node)
|
||||||
|
|
||||||
|
- **POST /v1/enqueue**
|
||||||
|
Body : `{ request_uid, folder_uid, office_uid, user_id, question, folder_context }`.
|
||||||
|
Réponse **202** : `{ request_uid }`.
|
||||||
|
Écrit dans `projects/<id>/data/notary-ai/pending/<safe_uid>.json`. **Authentification** : header `Authorization: Bearer <token>` obligatoire (le token identifie le projet) ; 401 si absent ou inconnu.
|
||||||
|
|
||||||
|
- **GET /v1/response/:request_uid**
|
||||||
|
Réponse **200** : `{ status: "pending" }` ou `{ status: "responded", response: { answer, nextActionsTable, membersInfoSheet, synthesisRecommendation } }`.
|
||||||
|
Lit d’abord `projects/<id>/data/notary-ai/responded/`, sinon `pending/`. **Authentification** : idem (token obligatoire).
|
||||||
|
|
||||||
|
- **GET /v1/health** et **GET /health** : santé du service (sans authentification).
|
||||||
|
|
||||||
|
Lancement : depuis **ia_dev** : `cd ia_dev && node ai_working_help/server.js`. Port par défaut : **3020** (`AI_WORKING_HELP_PORT`).
|
||||||
|
|
||||||
|
## Configuration côté application déployée
|
||||||
|
|
||||||
|
- **NOTARY_AI_AGENT_URL** : URL de base **sans** slug, sans slash final, ex. : `http://192.168.1.173:3020/v1`. Le backend ajoute `/enqueue` et `/response/:request_uid`.
|
||||||
|
- **NOTARY_AI_AGENT_TOKEN** : le token envoyé par l’app est de la forme **base** + **env**. **env** est le nom d’environnement (test, pprod, prod), à modifier selon les environnements. Côté ia_dev, ce token est trouvé en parcourant tous les projets et tous les envs (fichiers `projects/<id>/.secrets/<env>/ia_token`) ; le fichier peut contenir le token complet (ex. `nicolecoffreiotest`) ou la base seule (ex. `nicolecoffreio`), le serveur comparant alors le Bearer à `contenu_du_fichier + env`.
|
||||||
|
|
||||||
|
## Scripts (depuis la racine du dépôt, parent de ia_dev)
|
||||||
|
|
||||||
|
- **./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh** : liste les fichiers `projects/<id>/data/notary-ai/pending/*.json` (id résolu par AI_AGENT_TOKEN, IA_PROJECT, ai_project_id ou .ia_project).
|
||||||
|
- **./ia_dev/ai_working_help/notary-ai/write-response-notary-ai.sh** : écrit la réponse dans responded/, supprime le pending.
|
||||||
|
Options : `--pending-path <path> --response-json <json>` ou `--request-uid <uid> --answer "..." [--next-actions-table "..." ] [--members-info-sheet "..."] [--synthesis-recommendation "..."]`.
|
||||||
|
|
||||||
|
## Agents Cursor
|
||||||
|
|
||||||
|
- **notary-ai-loop** (`.cursor/agents/notary-ai-loop.md`) : orchestre la boucle (liste pending, lance notary-ai-process ; x cycles ou une fois).
|
||||||
|
- **notary-ai-process** (`.cursor/agents/notary-ai-process.md`) : pour chaque pending, produit les 4 champs et appelle write-response-notary-ai.sh.
|
||||||
|
|
||||||
|
Les agents sont dans **ia_dev/.cursor/agents/**. À invoquer depuis le dépôt qui contient ia_dev (racine = parent de ia_dev) ; les scripts s'exécutent depuis cette racine.
|
||||||
33
ai_working_help/notary-ai/lib.sh
Normal file
33
ai_working_help/notary-ai/lib.sh
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Shared config for notary-ai spooler scripts (pending/responded).
|
||||||
|
# Source from notary-ai/*.sh. Resolves PROJECT_SLUG and data dirs under projects/<slug>/data/notary-ai/.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
IA_DEV_ROOT="$(cd "${NOTARY_AI_DIR}/../.." && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${IA_DEV_ROOT}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT IA_DEV_ROOT
|
||||||
|
if [[ -f "${IA_DEV_ROOT}/lib/project_config.sh" ]]; then
|
||||||
|
# shellcheck source=../../lib/project_config.sh
|
||||||
|
source "${IA_DEV_ROOT}/lib/project_config.sh"
|
||||||
|
fi
|
||||||
|
if [[ -z "${PROJECT_SLUG:-}" ]]; then
|
||||||
|
PROJECT_SLUG="${AI_PROJECT_SLUG:-}"
|
||||||
|
fi
|
||||||
|
if [[ -n "${PROJECT_SLUG:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
|
||||||
|
DATA_NOTARY_AI_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/data/notary-ai"
|
||||||
|
DATA_NOTARY_AI_PENDING_DIR="${DATA_NOTARY_AI_DIR}/pending"
|
||||||
|
DATA_NOTARY_AI_RESPONDED_DIR="${DATA_NOTARY_AI_DIR}/responded"
|
||||||
|
mkdir -p "${DATA_NOTARY_AI_PENDING_DIR}" "${DATA_NOTARY_AI_RESPONDED_DIR}"
|
||||||
|
else
|
||||||
|
DATA_NOTARY_AI_DIR=""
|
||||||
|
DATA_NOTARY_AI_PENDING_DIR=""
|
||||||
|
DATA_NOTARY_AI_RESPONDED_DIR=""
|
||||||
|
fi
|
||||||
|
export DATA_NOTARY_AI_DIR
|
||||||
|
export DATA_NOTARY_AI_PENDING_DIR
|
||||||
|
export DATA_NOTARY_AI_RESPONDED_DIR
|
||||||
|
export PROJECT_SLUG
|
||||||
|
export IA_DEV_ROOT
|
||||||
|
export PROJECT_ROOT
|
||||||
19
ai_working_help/notary-ai/list-pending-notary-ai.sh
Executable file
19
ai_working_help/notary-ai/list-pending-notary-ai.sh
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# List pending JSON files in projects/<slug>/data/notary-ai/pending/.
|
||||||
|
# Run from repo root (parent of ia_dev). Output: one path per line.
|
||||||
|
# Usage: ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh
|
||||||
|
set -euo pipefail
|
||||||
|
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export NOTARY_AI_DIR
|
||||||
|
ROOT="$(cd "${NOTARY_AI_DIR}/../../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${NOTARY_AI_DIR}/lib.sh"
|
||||||
|
if [[ -z "${DATA_NOTARY_AI_PENDING_DIR:-}" || ! -d "${DATA_NOTARY_AI_PENDING_DIR}" ]]; then
|
||||||
|
echo "[notary-ai] DATA_NOTARY_AI_PENDING_DIR not set or not a directory. Set IA_PROJECT or ai_project_id at repo root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for f in "${DATA_NOTARY_AI_PENDING_DIR}"/*.json; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
echo "$f"
|
||||||
|
done
|
||||||
81
ai_working_help/notary-ai/write-response-notary-ai.sh
Executable file
81
ai_working_help/notary-ai/write-response-notary-ai.sh
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Write agent response to responded/ and remove the pending file.
|
||||||
|
# Usage:
|
||||||
|
# --pending-path <path> --response-json <json_string>
|
||||||
|
# or --request-uid <uid> --answer "..." [--next-actions-table "..." ] [--members-info-sheet "..."] [--synthesis-recommendation "..."]
|
||||||
|
set -euo pipefail
|
||||||
|
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export NOTARY_AI_DIR
|
||||||
|
ROOT="$(cd "${NOTARY_AI_DIR}/../../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${NOTARY_AI_DIR}/lib.sh"
|
||||||
|
if [[ -z "${DATA_NOTARY_AI_RESPONDED_DIR:-}" || ! -d "${DATA_NOTARY_AI_RESPONDED_DIR}" ]]; then
|
||||||
|
echo "[notary-ai] DATA_NOTARY_AI_RESPONDED_DIR not set or not a directory." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
require_jq() {
|
||||||
|
command -v jq &>/dev/null || { echo "[notary-ai] jq is required." >&2; return 1; }
|
||||||
|
}
|
||||||
|
PENDING_PATH=""
|
||||||
|
RESPONSE_JSON=""
|
||||||
|
REQUEST_UID=""
|
||||||
|
ANSWER=""
|
||||||
|
NEXT_ACTIONS_TABLE=""
|
||||||
|
MEMBERS_INFO_SHEET=""
|
||||||
|
SYNTHESIS_RECOMMENDATION=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--pending-path) PENDING_PATH="$2"; shift 2 ;;
|
||||||
|
--response-json) RESPONSE_JSON="$2"; shift 2 ;;
|
||||||
|
--request-uid) REQUEST_UID="$2"; shift 2 ;;
|
||||||
|
--answer) ANSWER="$2"; shift 2 ;;
|
||||||
|
--next-actions-table) NEXT_ACTIONS_TABLE="$2"; shift 2 ;;
|
||||||
|
--members-info-sheet) MEMBERS_INFO_SHEET="$2"; shift 2 ;;
|
||||||
|
--synthesis-recommendation) SYNTHESIS_RECOMMENDATION="$2"; shift 2 ;;
|
||||||
|
*) echo "[notary-ai] Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -n "${PENDING_PATH:-}" && -n "${RESPONSE_JSON:-}" ]]; then
|
||||||
|
require_jq || exit 1
|
||||||
|
[[ -f "$PENDING_PATH" ]] || { echo "[notary-ai] Pending file not found: $PENDING_PATH" >&2; exit 1; }
|
||||||
|
BASE="$(basename "$PENDING_PATH" .json)"
|
||||||
|
RESPONDED_PATH="${DATA_NOTARY_AI_RESPONDED_DIR}/${BASE}.json"
|
||||||
|
PENDING_DATA="$(jq -c . "$PENDING_PATH")"
|
||||||
|
RESPONSE_OBJ="$(printf '%s' "$RESPONSE_JSON" | jq -c .)"
|
||||||
|
# Merge: pending payload + status responded + response object
|
||||||
|
printf '%s' "$PENDING_DATA" | jq -c --argjson resp "$RESPONSE_OBJ" '. + { status: "responded", response: $resp }' > "${RESPONDED_PATH}"
|
||||||
|
rm -f "$PENDING_PATH"
|
||||||
|
echo "[notary-ai] Wrote ${RESPONDED_PATH}, removed pending."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ -n "${REQUEST_UID:-}" && -n "${ANSWER:-}" ]]; then
|
||||||
|
require_jq || exit 1
|
||||||
|
# Find pending file containing this request_uid
|
||||||
|
FOUND=""
|
||||||
|
for f in "${DATA_NOTARY_AI_PENDING_DIR}"/*.json; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
if [[ "$(jq -r '.request_uid // ""' "$f")" = "$REQUEST_UID" ]]; then
|
||||||
|
FOUND="$f"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ -n "$FOUND" ]] || { echo "[notary-ai] No pending file with request_uid=$REQUEST_UID" >&2; exit 1; }
|
||||||
|
RESPONSE_OBJ=$(jq -n \
|
||||||
|
--arg a "$ANSWER" \
|
||||||
|
--arg nat "${NEXT_ACTIONS_TABLE:-}" \
|
||||||
|
--arg mis "${MEMBERS_INFO_SHEET:-}" \
|
||||||
|
--arg sr "${SYNTHESIS_RECOMMENDATION:-}" \
|
||||||
|
'{ answer: $a, nextActionsTable: $nat, membersInfoSheet: $mis, synthesisRecommendation: $sr }')
|
||||||
|
PENDING_PATH="$FOUND"
|
||||||
|
RESPONSE_JSON="$RESPONSE_OBJ"
|
||||||
|
BASE="$(basename "$PENDING_PATH" .json)"
|
||||||
|
RESPONDED_PATH="${DATA_NOTARY_AI_RESPONDED_DIR}/${BASE}.json"
|
||||||
|
PENDING_DATA="$(jq -c . "$PENDING_PATH")"
|
||||||
|
printf '%s' "$PENDING_DATA" | jq -c --argjson resp "$RESPONSE_OBJ" '. + { status: "responded", response: $resp }' > "${RESPONDED_PATH}"
|
||||||
|
rm -f "$PENDING_PATH"
|
||||||
|
echo "[notary-ai] Wrote ${RESPONDED_PATH}, removed pending."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "[notary-ai] Use --pending-path + --response-json or --request-uid + --answer (and optional fields)." >&2
|
||||||
|
exit 1
|
||||||
13
ai_working_help/package.json
Normal file
13
ai_working_help/package.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ai_working_help",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API spooler for notary AI agent (enqueue / response). Consumed by business app backends; responses produced by Cursor agents (notary-ai-loop + notary-ai-process).",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
163
ai_working_help/server.js
Normal file
163
ai_working_help/server.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* ai_working_help API server.
|
||||||
|
* Routes: POST /v1/enqueue, GET /v1/response/:request_uid, GET /health, GET /v1/health.
|
||||||
|
* Project id and env are resolved from the Bearer token by searching all
|
||||||
|
* projects/<id>/.secrets/<env>/ia_token files; the matching project id is used for the spooler.
|
||||||
|
* Spooler: projects/<id>/data/notary-ai/{pending,responded}.
|
||||||
|
*/
|
||||||
|
const express = require("express");
|
||||||
|
const bodyParser = require("body-parser");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json({ limit: "1mb" }));
|
||||||
|
|
||||||
|
const PORT = process.env.AI_WORKING_HELP_PORT || 3020;
|
||||||
|
const IA_DEV_ROOT = path.resolve(__dirname, "..");
|
||||||
|
const PROJECTS_DIR = path.join(IA_DEV_ROOT, "projects");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve project id and env from token by scanning projects/<id>/.secrets/<env>/ia_token.
|
||||||
|
* @returns {{ projectId: string, env: string } | null}
|
||||||
|
*/
|
||||||
|
function resolveProjectAndEnvByToken(token) {
|
||||||
|
if (!token || typeof token !== "string") return null;
|
||||||
|
const t = token.trim();
|
||||||
|
if (!t) return null;
|
||||||
|
const dirs = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true });
|
||||||
|
for (const d of dirs) {
|
||||||
|
if (!d.isDirectory()) continue;
|
||||||
|
const secretsDir = path.join(PROJECTS_DIR, d.name, ".secrets");
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(secretsDir) || !fs.statSync(secretsDir).isDirectory()) continue;
|
||||||
|
const envDirs = fs.readdirSync(secretsDir, { withFileTypes: true });
|
||||||
|
for (const ed of envDirs) {
|
||||||
|
if (!ed.isDirectory()) continue;
|
||||||
|
const tokenPath = path.join(secretsDir, ed.name, "ia_token");
|
||||||
|
if (!fs.existsSync(tokenPath) || !fs.statSync(tokenPath).isFile()) continue;
|
||||||
|
const content = fs.readFileSync(tokenPath, "utf8").trim();
|
||||||
|
const envName = ed.name;
|
||||||
|
// Token is either full value in file (content) or base in file + env suffix: nicolecoffreio<env>
|
||||||
|
if (content === t || content + envName === t) return { projectId: d.name, env: envName };
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProjectIdByToken(token) {
|
||||||
|
const resolved = resolveProjectAndEnvByToken(token);
|
||||||
|
return resolved ? resolved.projectId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireApiTokenAndResolveProject(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || typeof authHeader !== "string") {
|
||||||
|
return res.status(401).json({ message: "Invalid or missing token" });
|
||||||
|
}
|
||||||
|
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
||||||
|
const token = match ? String(match[1]).trim() : "";
|
||||||
|
const resolved = resolveProjectAndEnvByToken(token);
|
||||||
|
if (!resolved) {
|
||||||
|
return res.status(401).json({ message: "Invalid or missing token" });
|
||||||
|
}
|
||||||
|
req.projectId = resolved.projectId;
|
||||||
|
req.projectEnv = resolved.env;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeUid(uid) {
|
||||||
|
return String(uid).replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 128) || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectDataDir(projectId) {
|
||||||
|
if (!projectId || /[^a-zA-Z0-9-_]/.test(projectId)) return null;
|
||||||
|
return path.join(PROJECTS_DIR, projectId, "data", "notary-ai");
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFileByRequestUid(dir, requestUid) {
|
||||||
|
if (!fs.existsSync(dir)) return null;
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const f of files) {
|
||||||
|
if (!f.endsWith(".json")) continue;
|
||||||
|
const filePath = path.join(dir, f);
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
|
if (data.request_uid === requestUid) return { path: filePath, data };
|
||||||
|
} catch (_) {
|
||||||
|
// skip invalid json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
res.status(200).json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/v1/health", (req, res) => {
|
||||||
|
res.status(200).json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/v1/enqueue", requireApiTokenAndResolveProject, (req, res) => {
|
||||||
|
const dir = projectDataDir(req.projectId);
|
||||||
|
if (!dir) {
|
||||||
|
return res.status(400).json({ message: "Invalid project" });
|
||||||
|
}
|
||||||
|
const pendingDir = path.join(dir, "pending");
|
||||||
|
const respondedDir = path.join(dir, "responded");
|
||||||
|
const body = req.body || {};
|
||||||
|
const requestUid = body.request_uid;
|
||||||
|
if (!requestUid || typeof requestUid !== "string") {
|
||||||
|
return res.status(400).json({ message: "Missing request_uid" });
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
request_uid: requestUid,
|
||||||
|
folder_uid: body.folder_uid,
|
||||||
|
office_uid: body.office_uid,
|
||||||
|
user_id: body.user_id,
|
||||||
|
question: body.question,
|
||||||
|
folder_context: body.folder_context || {},
|
||||||
|
status: "pending",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true });
|
||||||
|
if (!fs.existsSync(respondedDir)) fs.mkdirSync(respondedDir, { recursive: true });
|
||||||
|
const safe = safeUid(requestUid);
|
||||||
|
const filePath = path.join(pendingDir, `${safe}.json`);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
|
||||||
|
res.status(202).json({ request_uid: requestUid });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ai_working_help] enqueue write error", err);
|
||||||
|
res.status(500).json({ message: "Write failed" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/v1/response/:request_uid", requireApiTokenAndResolveProject, (req, res) => {
|
||||||
|
const requestUid = req.params.request_uid;
|
||||||
|
const dir = projectDataDir(req.projectId);
|
||||||
|
if (!dir) {
|
||||||
|
return res.status(400).json({ message: "Invalid project" });
|
||||||
|
}
|
||||||
|
const respondedDir = path.join(dir, "responded");
|
||||||
|
const foundResponded = findFileByRequestUid(respondedDir, requestUid);
|
||||||
|
if (foundResponded && foundResponded.data.response) {
|
||||||
|
return res.status(200).json({
|
||||||
|
status: "responded",
|
||||||
|
response: foundResponded.data.response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const pendingDir = path.join(dir, "pending");
|
||||||
|
const foundPending = findFileByRequestUid(pendingDir, requestUid);
|
||||||
|
if (foundPending) {
|
||||||
|
return res.status(200).json({ status: "pending" });
|
||||||
|
}
|
||||||
|
res.status(200).json({ status: "pending" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`[ai_working_help] listening on port ${PORT}`);
|
||||||
|
});
|
||||||
17
deploy/_lib/colors.sh
Normal file
17
deploy/_lib/colors.sh
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_ts_utc() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
|
||||||
|
|
||||||
|
success() { echo -e "[$(log_ts_utc)] ${GREEN}✔${NC} $1"; }
|
||||||
|
error() { echo -e "[$(log_ts_utc)] ${RED}✗${NC} $1"; }
|
||||||
|
warning() { echo -e "[$(log_ts_utc)] ${YELLOW}⚠${NC} $1"; }
|
||||||
|
info() { echo -e "[$(log_ts_utc)] ${BLUE}ℹ${NC} $1"; }
|
||||||
66
deploy/_lib/env-map.sh
Normal file
66
deploy/_lib/env-map.sh
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#
|
||||||
|
# Environment mapping for LeCoffre.io v2 deployments (proxy-based infra).
|
||||||
|
# - Proxy (jump/orchestrator): 192.168.1.100 (4nk.myftp.biz)
|
||||||
|
# - Targets: test=192.168.1.101, pprod=192.168.1.102, prod=192.168.1.103, services=192.168.1.104
|
||||||
|
#
|
||||||
|
|
||||||
|
get_env_target_ip() {
|
||||||
|
local env="$1"
|
||||||
|
case "$env" in
|
||||||
|
test) echo "192.168.1.101" ;;
|
||||||
|
pprod) echo "192.168.1.102" ;;
|
||||||
|
prod) echo "192.168.1.103" ;;
|
||||||
|
services) echo "192.168.1.104" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_env_domain() {
|
||||||
|
local env="$1"
|
||||||
|
case "$env" in
|
||||||
|
test) echo "test.lecoffreio.4nkweb.com" ;;
|
||||||
|
pprod) echo "pprod.lecoffreio.4nkweb.com" ;;
|
||||||
|
prod) echo "prod.lecoffreio.4nkweb.com" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Repository path on each target host (infra standard: /srv/4NK/<domain>/)
|
||||||
|
get_env_remote_app_root() {
|
||||||
|
local env="$1"
|
||||||
|
local domain
|
||||||
|
domain="$(get_env_domain "$env")"
|
||||||
|
echo "/srv/4NK/${domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Public service port (proxied by nginx on proxy).
|
||||||
|
# This port is reserved for LeCoffre.io in the infra ports map.
|
||||||
|
get_env_service_port() {
|
||||||
|
local env="$1"
|
||||||
|
case "$env" in
|
||||||
|
test|pprod|prod) echo "3009" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal frontend port (served by Next.js, proxied by local router).
|
||||||
|
get_env_frontend_internal_port() {
|
||||||
|
local env="$1"
|
||||||
|
case "$env" in
|
||||||
|
test|pprod|prod) echo "3100" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal backend port (served by Express, proxied by local router).
|
||||||
|
get_env_backend_internal_port() {
|
||||||
|
local env="$1"
|
||||||
|
case "$env" in
|
||||||
|
test|pprod|prod) echo "3101" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
293
deploy/_lib/git-flow.sh
Normal file
293
deploy/_lib/git-flow.sh
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Git flow functions for automatic branch promotion and verification
|
||||||
|
#
|
||||||
|
# Prerequisites: This file must be sourced after env-map.sh and ssh.sh
|
||||||
|
# Functions used: get_env_target_ip, get_env_service_port, get_env_backend_internal_port, ssh_run
|
||||||
|
# Variables used: DEPLOY_SSH_KEY, DEPLOY_SSH_USER
|
||||||
|
|
||||||
|
# Vérifie le succès d'un déploiement
|
||||||
|
|
||||||
|
verify_deployment_success() {
|
||||||
|
local env="$1"
|
||||||
|
local domain="$2"
|
||||||
|
local ssh_key="${DEPLOY_SSH_KEY:-$HOME/.ssh/id_ed25519}"
|
||||||
|
local ssh_user="${DEPLOY_SSH_USER:-ncantu}"
|
||||||
|
local target_ip
|
||||||
|
local service_port
|
||||||
|
local backend_internal_port
|
||||||
|
|
||||||
|
# These functions should be available from env-map.sh (sourced before this file)
|
||||||
|
target_ip="$(get_env_target_ip "$env")"
|
||||||
|
service_port="$(get_env_service_port "$env")"
|
||||||
|
backend_internal_port="$(get_env_backend_internal_port "$env")"
|
||||||
|
|
||||||
|
# 1. Attendre quelques secondes pour que les services démarrent
|
||||||
|
info "[verify] Waiting for services to start (15 seconds)..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# 2. Health check HTTP avec retries
|
||||||
|
info "[verify] Checking health endpoint via router (port ${service_port})..."
|
||||||
|
local health_status
|
||||||
|
local max_retries=3
|
||||||
|
local retry_count=0
|
||||||
|
|
||||||
|
# Vérifier via le router depuis le serveur distant (via SSH)
|
||||||
|
# Le router route /api/ vers le backend
|
||||||
|
while [[ $retry_count -lt $max_retries ]]; do
|
||||||
|
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${service_port}/api/v1/public/health' 2>/dev/null || echo '000'")
|
||||||
|
|
||||||
|
if [[ "$health_status" == "200" ]]; then
|
||||||
|
info "[verify] Health check passed via router (HTTP $health_status)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
retry_count=$((retry_count + 1))
|
||||||
|
if [[ $retry_count -lt $max_retries ]]; then
|
||||||
|
info "[verify] Health check attempt $retry_count failed (HTTP $health_status), retrying in 5 seconds..."
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$health_status" != "200" ]]; then
|
||||||
|
# Essayer directement le backend en fallback
|
||||||
|
info "[verify] Router check failed (HTTP $health_status), trying backend directly (port ${backend_internal_port})..."
|
||||||
|
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${backend_internal_port}/api/v1/public/health' 2>/dev/null || echo '000'")
|
||||||
|
|
||||||
|
# If 404, backend may mount API at root (API_ROOT_URL=/); try path without /api prefix
|
||||||
|
if [[ "$health_status" == "404" ]]; then
|
||||||
|
info "[verify] Backend returned 404 for /api/v1/public/health, trying /v1/public/health..."
|
||||||
|
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${backend_internal_port}/v1/public/health' 2>/dev/null || echo '000'")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$health_status" != "200" ]]; then
|
||||||
|
error "[verify] Health check failed: HTTP $health_status"
|
||||||
|
|
||||||
|
# Afficher les logs du backend pour diagnostic
|
||||||
|
info "[verify] Backend logs (last 50 lines):"
|
||||||
|
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"journalctl -u lecoffreio-backend@${domain}.service --no-pager -n 50 2>/dev/null || true" | sed 's/^/ /'
|
||||||
|
|
||||||
|
# Afficher l'état des services
|
||||||
|
info "[verify] Service status:"
|
||||||
|
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"systemctl status lecoffreio-backend@${domain}.service lecoffreio-router@${domain}.service --no-pager -l 2>/dev/null || true" | sed 's/^/ /'
|
||||||
|
|
||||||
|
# Vérifier si le port est en écoute
|
||||||
|
info "[verify] Checking if backend port ${backend_internal_port} is listening:"
|
||||||
|
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"ss -tlnp | grep ':${backend_internal_port}' || echo ' Port ${backend_internal_port} is not listening'" | sed 's/^/ /'
|
||||||
|
|
||||||
|
error "[verify] Backend may not be fully started yet. Check logs: journalctl -u lecoffreio-backend@${domain}.service -n 50"
|
||||||
|
error "[verify] Router status: systemctl status lecoffreio-router@${domain}.service"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
info "[verify] Health check passed via direct backend (HTTP $health_status)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Vérification des services systemd avec retries (frontend peut prendre plus de temps)
|
||||||
|
info "[verify] Checking systemd services..."
|
||||||
|
local services_status
|
||||||
|
local max_service_retries=10
|
||||||
|
local service_retry_count=0
|
||||||
|
local all_active=false
|
||||||
|
|
||||||
|
while [[ $service_retry_count -lt $max_service_retries && "$all_active" != "true" ]]; do
|
||||||
|
services_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null | grep -vE '^(active|activating)$' || true")
|
||||||
|
|
||||||
|
if [[ -z "$services_status" ]]; then
|
||||||
|
# Vérifier que tous les services sont vraiment "active" (pas "activating")
|
||||||
|
local all_status
|
||||||
|
all_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null")
|
||||||
|
|
||||||
|
# Vérifier s'il y a des erreurs dans les logs du frontend (si en "activating")
|
||||||
|
if echo "$all_status" | grep -q "activating"; then
|
||||||
|
# Vérifier les logs du frontend pour voir s'il y a une erreur
|
||||||
|
local frontend_errors
|
||||||
|
frontend_errors=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"journalctl -u lecoffreio-frontend@${domain}.service --since '2 minutes ago' --no-pager 2>/dev/null | { grep -iE '(error|fatal|failed)' || true; } | tail -5")
|
||||||
|
|
||||||
|
if [[ -n "$frontend_errors" ]]; then
|
||||||
|
error "[verify] Frontend errors detected while activating:"
|
||||||
|
echo "$frontend_errors" | sed 's/^/ /'
|
||||||
|
error "[verify] Check frontend logs: journalctl -u lecoffreio-frontend@${domain}.service -n 50"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
service_retry_count=$((service_retry_count + 1))
|
||||||
|
if [[ $service_retry_count -lt $max_service_retries ]]; then
|
||||||
|
info "[verify] Some services still activating, waiting 10 seconds (attempt $service_retry_count/$max_service_retries)..."
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
all_active=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
service_retry_count=$((service_retry_count + 1))
|
||||||
|
if [[ $service_retry_count -lt $max_service_retries ]]; then
|
||||||
|
info "[verify] Some services not active, waiting 10 seconds (attempt $service_retry_count/$max_service_retries)..."
|
||||||
|
echo "$services_status" | sed 's/^/ /'
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$all_active" != "true" ]]; then
|
||||||
|
# Dernière vérification pour afficher l'état final et les logs
|
||||||
|
services_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null || echo 'unknown'")
|
||||||
|
error "[verify] Some services are not active after $max_service_retries attempts:"
|
||||||
|
echo "$services_status" | sed 's/^/ /'
|
||||||
|
|
||||||
|
# Afficher les logs du frontend si toujours en activating
|
||||||
|
if echo "$services_status" | grep -q "activating.*frontend"; then
|
||||||
|
info "[verify] Frontend logs (last 30 lines):"
|
||||||
|
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"journalctl -u lecoffreio-frontend@${domain}.service --no-pager -n 30 2>/dev/null || true" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
error "[verify] Check service status: systemctl status lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "[verify] All systemd services are active"
|
||||||
|
|
||||||
|
# 3. Vérification des logs (erreurs critiques récentes)
|
||||||
|
info "[verify] Checking for critical errors in logs..."
|
||||||
|
local critical_errors
|
||||||
|
critical_errors=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
|
||||||
|
"journalctl -u lecoffreio-backend@${domain}.service --since '5 minutes ago' --no-pager 2>/dev/null | { grep -iE '(error|fatal|critical)' || true; } | tail -10")
|
||||||
|
|
||||||
|
if [[ -n "$critical_errors" ]]; then
|
||||||
|
warning "[verify] Critical errors found in recent logs:"
|
||||||
|
echo "$critical_errors" | sed 's/^/ /'
|
||||||
|
# Ne pas bloquer pour les warnings, seulement les erreurs fatales
|
||||||
|
# On pourrait ajouter une logique plus fine ici
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "[verify] Deployment verification passed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Détermine l'environnement suivant dans la chaîne
|
||||||
|
get_next_env() {
|
||||||
|
local current_env="$1"
|
||||||
|
case "$current_env" in
|
||||||
|
dev) echo "test" ;;
|
||||||
|
test) echo "pprod" ;;
|
||||||
|
pprod) echo "prod" ;;
|
||||||
|
prod) echo "" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Promotion automatique vers l'environnement suivant
|
||||||
|
auto_promote_to_next_env() {
|
||||||
|
local current_env="$1"
|
||||||
|
local current_branch="$2"
|
||||||
|
local project_root="$3"
|
||||||
|
local deploy_git_remote="${4:-lecoffre_ng}"
|
||||||
|
local next_env
|
||||||
|
local next_branch
|
||||||
|
|
||||||
|
# Si on n'est pas sur dev, pas de promotion
|
||||||
|
if [[ "$current_branch" != "dev" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
next_env=$(get_next_env "$current_env")
|
||||||
|
if [[ -z "$next_env" ]]; then
|
||||||
|
info "[promote] No next environment (already at prod)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Déterminer la branche cible
|
||||||
|
case "$next_env" in
|
||||||
|
test) next_branch="test" ;;
|
||||||
|
pprod) next_branch="pprod" ;;
|
||||||
|
prod) next_branch="prod" ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
info "[promote] Auto-promoting dev → $next_branch for $next_env environment..."
|
||||||
|
|
||||||
|
# 1. Fetch la branche cible
|
||||||
|
git -C "$project_root" fetch "$deploy_git_remote" "$next_branch" || true
|
||||||
|
|
||||||
|
# 2. Checkout la branche cible
|
||||||
|
git -C "$project_root" checkout "$next_branch" || {
|
||||||
|
# Branch doesn't exist locally, create it from remote
|
||||||
|
git -C "$project_root" checkout -b "$next_branch" "${deploy_git_remote}/${next_branch}" 2>/dev/null || {
|
||||||
|
# Remote branch doesn't exist, create new branch
|
||||||
|
git -C "$project_root" checkout -b "$next_branch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Merge dev into target branch
|
||||||
|
if ! git -C "$project_root" merge dev --allow-unrelated-histories --no-edit; then
|
||||||
|
error "[promote] Merge dev → $next_branch failed. Resolve conflicts manually."
|
||||||
|
git -C "$project_root" checkout dev
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Push
|
||||||
|
info "[promote] Pushing $next_branch..."
|
||||||
|
git -C "$project_root" push "$deploy_git_remote" "$next_branch"
|
||||||
|
|
||||||
|
# 5. Retourner sur dev
|
||||||
|
info "[promote] Returning to dev branch..."
|
||||||
|
git -C "$project_root" checkout dev
|
||||||
|
|
||||||
|
success "[promote] Successfully promoted dev → $next_branch"
|
||||||
|
info "[promote] Next step: deploy to $next_env with: ./deploy/scripts_v2/deploy.sh $next_env"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stage all changes, commit with message, and push current branch
|
||||||
|
# Usage: git_add_commit_push <project_root> <commit_message> [remote]
|
||||||
|
# Example: git_add_commit_push /path/to/repo "fix: something"
|
||||||
|
git_add_commit_push() {
|
||||||
|
local project_root="${1:-.}"
|
||||||
|
local commit_message="$2"
|
||||||
|
local deploy_git_remote="${3:-lecoffre_ng}"
|
||||||
|
local current_branch
|
||||||
|
|
||||||
|
if [[ -z "$commit_message" ]]; then
|
||||||
|
error "[git] Commit message required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lint --fix on all projects before staging (resources, backend, frontend). Non-blocking.
|
||||||
|
info "[lint] Running lint --fix on lecoffre-ressources-dev, lecoffre-back-main, lecoffre-front-main..."
|
||||||
|
(cd "${project_root}/lecoffre-ressources-dev" && npm run lint:fix) || warning "[lint] lecoffre-ressources-dev lint:fix failed (non-blocking)"
|
||||||
|
(cd "${project_root}/lecoffre-back-main" && npm run lint:fix) || warning "[lint] lecoffre-back-main lint:fix failed (non-blocking)"
|
||||||
|
(cd "${project_root}/lecoffre-front-main" && npm run lint:fix) || warning "[lint] lecoffre-front-main lint:fix failed (non-blocking)"
|
||||||
|
info "[lint] Lint:fix step done"
|
||||||
|
|
||||||
|
info "[git] Staging all changes (add -A)..."
|
||||||
|
git -C "$project_root" add -A || {
|
||||||
|
error "[git] git add -A failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info "[git] Committing..."
|
||||||
|
git -C "$project_root" commit -m "$commit_message" || {
|
||||||
|
error "[git] commit failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
current_branch=$(git -C "$project_root" branch --show-current)
|
||||||
|
info "[git] Pushing to $deploy_git_remote $current_branch..."
|
||||||
|
git -C "$project_root" push "$deploy_git_remote" "$current_branch" || {
|
||||||
|
error "[git] push failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success "[git] add -A, commit, push done"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
95
deploy/_lib/ssh.sh
Normal file
95
deploy/_lib/ssh.sh
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
require_ssh_key() {
|
||||||
|
local key_path="$1"
|
||||||
|
if [[ -z "$key_path" ]]; then
|
||||||
|
echo "SSH key path is required" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$key_path" ]]; then
|
||||||
|
echo "SSH key not found: $key_path" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_common_opts() {
|
||||||
|
local ssh_user="$1"
|
||||||
|
local ssh_host="$2"
|
||||||
|
|
||||||
|
# Keepalive to reduce flakiness through ProxyJump
|
||||||
|
# (observed: "Connection reset by peer" during scp/ssh).
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - Avoid SSH multiplexing here: ControlPath/ControlMaster can be flaky on Windows OpenSSH + MSYS paths.
|
||||||
|
# - Increased timeouts and keepalive settings to handle network instability
|
||||||
|
# - Compression disabled to reduce overhead and potential connection issues
|
||||||
|
echo \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o ConnectTimeout=30 \
|
||||||
|
-o ServerAliveInterval=10 \
|
||||||
|
-o ServerAliveCountMax=6 \
|
||||||
|
-o TCPKeepAlive=yes \
|
||||||
|
-o Compression=no
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_run() {
|
||||||
|
local ssh_key="$1"
|
||||||
|
local ssh_user="$2"
|
||||||
|
local ssh_host="$3"
|
||||||
|
shift 3
|
||||||
|
|
||||||
|
require_ssh_key "$ssh_key"
|
||||||
|
|
||||||
|
local proxy_host="${DEPLOY_SSH_PROXY_HOST:-}"
|
||||||
|
local proxy_user="${DEPLOY_SSH_PROXY_USER:-$ssh_user}"
|
||||||
|
|
||||||
|
local proxy_args=()
|
||||||
|
if [[ -n "$proxy_host" ]]; then
|
||||||
|
proxy_args=(-J "$proxy_user@$proxy_host")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2207
|
||||||
|
local common_opts=($(ssh_common_opts "$ssh_user" "$ssh_host"))
|
||||||
|
|
||||||
|
ssh -i "$ssh_key" \
|
||||||
|
"${common_opts[@]}" \
|
||||||
|
"${proxy_args[@]}" \
|
||||||
|
"$ssh_user@$ssh_host" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
scp_copy() {
|
||||||
|
local ssh_key="$1"
|
||||||
|
local src="$2"
|
||||||
|
local ssh_user="$3"
|
||||||
|
local ssh_host="$4"
|
||||||
|
local dst="$5"
|
||||||
|
local recursive="${6:-false}"
|
||||||
|
|
||||||
|
require_ssh_key "$ssh_key"
|
||||||
|
|
||||||
|
local proxy_host="${DEPLOY_SSH_PROXY_HOST:-}"
|
||||||
|
local proxy_user="${DEPLOY_SSH_PROXY_USER:-$ssh_user}"
|
||||||
|
|
||||||
|
local proxy_args=()
|
||||||
|
if [[ -n "$proxy_host" ]]; then
|
||||||
|
proxy_args=(-o "ProxyJump=$proxy_user@$proxy_host")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2207
|
||||||
|
local common_opts=($(ssh_common_opts "$ssh_user" "$ssh_host"))
|
||||||
|
|
||||||
|
local scp_opts=()
|
||||||
|
# Add -r for recursive copy if requested or if source is a directory
|
||||||
|
if [[ "$recursive" == "true" ]] || [[ -d "$src" ]]; then
|
||||||
|
scp_opts=(-r)
|
||||||
|
fi
|
||||||
|
|
||||||
|
scp -i "$ssh_key" \
|
||||||
|
"${scp_opts[@]}" \
|
||||||
|
"${common_opts[@]}" \
|
||||||
|
"${proxy_args[@]}" \
|
||||||
|
"$src" "$ssh_user@$ssh_host:$dst"
|
||||||
|
}
|
||||||
108
deploy/branch-align.sh
Executable file
108
deploy/branch-align.sh
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Aligns only origin/test, origin/pprod, origin/prod to current branch SHA. main is not aligned.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "[branch-align][ERROR] Not in a git repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
|
||||||
|
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
|
||||||
|
cd "$PROJECT_ROOT" && exec "${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
env_branch="${1:-}"
|
||||||
|
if [[ -z "$env_branch" ]]; then
|
||||||
|
echo "[branch-align][ERROR] Missing <env> argument (expected: main|test|pprod|prod)" >&2
|
||||||
|
echo "Usage: ./deploy/branch-align.sh <env>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$env_branch" =~ ^(main|test|pprod|prod)$ ]]; then
|
||||||
|
echo "[branch-align][ERROR] Invalid <env>: must be main, test, pprod or prod (got: '${env_branch}')" >&2
|
||||||
|
echo "Usage: ./deploy/branch-align.sh <env>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ "$current_branch" != "$env_branch" ]]; then
|
||||||
|
echo "[branch-align][ERROR] Must be on branch '${env_branch}' (current: '${current_branch}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch latest refs
|
||||||
|
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
target_sha="$(git rev-parse "$env_branch")"
|
||||||
|
origin_env_sha="$(git rev-parse "origin/${env_branch}")"
|
||||||
|
|
||||||
|
if [[ "$target_sha" != "$origin_env_sha" ]]; then
|
||||||
|
echo "[branch-align] origin/${env_branch} differs from local ${env_branch}. Updating remote to local (env priority)."
|
||||||
|
git push --force-with-lease origin "${target_sha}:${env_branch}"
|
||||||
|
git fetch origin
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Align all three branches to env SHA
|
||||||
|
for br in test pprod prod; do
|
||||||
|
if [[ "$br" == "$env_branch" ]]; then
|
||||||
|
# Ensure tracking exists
|
||||||
|
git branch --set-upstream-to="origin/${br}" "$br" >/dev/null 2>&1 || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
git branch -f "$br" "$target_sha"
|
||||||
|
git push --force-with-lease origin "${target_sha}:${br}"
|
||||||
|
git branch --set-upstream-to="origin/${br}" "$br" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also ensure env branch tracks its remote
|
||||||
|
|
||||||
|
git branch --set-upstream-to="origin/${env_branch}" "$env_branch" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Verify last 30 commits are identical
|
||||||
|
|
||||||
|
tmp1="$(mktemp -t branch-align-test.XXXXXX)"
|
||||||
|
tmp2="$(mktemp -t branch-align-pprod.XXXXXX)"
|
||||||
|
tmp3="$(mktemp -t branch-align-prod.XXXXXX)"
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$tmp1" "$tmp2" "$tmp3"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
git log -30 --format=%H origin/test > "$tmp1"
|
||||||
|
git log -30 --format=%H origin/pprod > "$tmp2"
|
||||||
|
git log -30 --format=%H origin/prod > "$tmp3"
|
||||||
|
|
||||||
|
if ! diff -u "$tmp1" "$tmp2" >/dev/null; then
|
||||||
|
echo "[branch-align][ERROR] Last 30 commits differ: origin/test vs origin/pprod" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! diff -u "$tmp1" "$tmp3" >/dev/null; then
|
||||||
|
echo "[branch-align][ERROR] Last 30 commits differ: origin/test vs origin/prod" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final assertions
|
||||||
|
|
||||||
|
if [[ "$(git rev-parse --abbrev-ref HEAD)" != "$env_branch" ]]; then
|
||||||
|
echo "[branch-align][ERROR] Branch changed unexpectedly" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sha_test="$(git rev-parse origin/test)"
|
||||||
|
sha_pprod="$(git rev-parse origin/pprod)"
|
||||||
|
sha_prod="$(git rev-parse origin/prod)"
|
||||||
|
|
||||||
|
if [[ "$sha_test" != "$sha_pprod" ]] || [[ "$sha_test" != "$sha_prod" ]]; then
|
||||||
|
echo "[branch-align][ERROR] Remote branches are not aligned" >&2
|
||||||
|
echo "origin/test=$sha_test" >&2
|
||||||
|
echo "origin/pprod=$sha_pprod" >&2
|
||||||
|
echo "origin/prod=$sha_prod" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[branch-align] OK: origin/test, origin/pprod, origin/prod aligned to ${sha_test}"
|
||||||
91
deploy/bump-version.sh
Normal file
91
deploy/bump-version.sh
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Bump version and optional package.json files from project config (projects/<id>/conf.json).
|
||||||
|
# Usage: ./bump-version.sh <version> [message_court]
|
||||||
|
# Requires: run from repo root; project id from IA_PROJECT, .ia_project, or ai_project_id; jq if using version.package_json_paths.
|
||||||
|
|
||||||
|
VERSION="${1:-}"
|
||||||
|
SHORT_MSG="${2:-Nouvelles fonctionnalités et améliorations}"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
echo "❌ Usage: ./bump-version.sh <version> [message_court]"
|
||||||
|
echo " Exemple: ./bump-version.sh 2.1.0 'Nouveaux filtres'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "❌ Version invalide. Format attendu: X.Y.Z (ex: 2.1.0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "❌ Not in a git repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
|
||||||
|
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||||
|
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
|
||||||
|
SCRIPT_ABS="${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
cd "$PROJECT_ROOT" && exec "$SCRIPT_ABS" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=../lib/project_config.sh
|
||||||
|
source "${IA_DEV_ROOT}/lib/project_config.sh"
|
||||||
|
|
||||||
|
echo "🔄 Mise à jour vers v${VERSION}..."
|
||||||
|
|
||||||
|
echo "$VERSION" > "$PROJECT_ROOT/VERSION"
|
||||||
|
echo "✅ VERSION → ${VERSION}"
|
||||||
|
|
||||||
|
package_paths=()
|
||||||
|
splash_name="Application"
|
||||||
|
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[[ -n "$p" ]] && package_paths+=( "$p" )
|
||||||
|
done < <(jq -r '.version.package_json_paths[]? // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)
|
||||||
|
splash_name="$(jq -r '.version.splash_app_name // "Application"' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for p in "${package_paths[@]}"; do
|
||||||
|
if [[ -f "$PROJECT_ROOT/$p" ]]; then
|
||||||
|
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$PROJECT_ROOT/$p"
|
||||||
|
echo "✅ $p → ${VERSION}"
|
||||||
|
else
|
||||||
|
echo "⚠️ $p not found, skipped"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -f "$PROJECT_ROOT/CHANGELOG.md" ]]; then
|
||||||
|
echo "⚠️ CHANGELOG.md non trouvé. Créez-le manuellement avec les détails de cette version."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "📋 PROCHAINES ÉTAPES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "1. Éditer CHANGELOG.md pour documenter les changements de v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "2. Mettre à jour le .env distant avec le message splash (si applicable) :"
|
||||||
|
echo ""
|
||||||
|
cat << EOF
|
||||||
|
NEXT_PUBLIC_SPLASH_MESSAGE="🎉 ${splash_name} v${VERSION} est disponible !
|
||||||
|
|
||||||
|
✨ ${SHORT_MSG}
|
||||||
|
|
||||||
|
📖 Consultez CHANGELOG.md pour tous les détails"
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SPLASH_MAX_DISPLAYS=10
|
||||||
|
NEXT_PUBLIC_SPLASH_ID="splash_v${VERSION}"
|
||||||
|
EOF
|
||||||
|
echo ""
|
||||||
|
echo "3. Rebuild, redémarrer et déployer selon le workflow du projet."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Bump terminé. Éditer CHANGELOG.md puis lancer le déploiement selon le projet."
|
||||||
|
|
||||||
|
exit 0
|
||||||
32
deploy/change-to-all-branches.sh
Executable file
32
deploy/change-to-all-branches.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# From branch test only: align origin/test, origin/pprod, origin/prod then deploy to test (import-v1, skipSetupHost).
|
||||||
|
# Use when you have already pushed to test and want to sync other branches and deploy test in one go.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "[change-to-all-branches][ERROR] Not in a git repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
|
||||||
|
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
|
||||||
|
cd "$PROJECT_ROOT" && exec "${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
current="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ "$current" != "test" ]]; then
|
||||||
|
echo "[change-to-all-branches][ERROR] Must be on branch 'test' (current: '${current}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[change-to-all-branches] Aligning branches..."
|
||||||
|
"$DEPLOY_DIR/branch-align.sh" test
|
||||||
|
|
||||||
|
# scripts_v2 lives in the host project's deploy/ (not necessarily under ia_dev)
|
||||||
|
DEPLOY_SCRIPTS_V2="${PROJECT_ROOT}/deploy/scripts_v2"
|
||||||
|
echo "[change-to-all-branches] Deploying test (--import-v1 --skipSetupHost, --no-sync-origin because we just pushed)..."
|
||||||
|
"${DEPLOY_SCRIPTS_V2}/deploy.sh" test --import-v1 --skipSetupHost --no-sync-origin
|
||||||
|
|
||||||
|
echo "[change-to-all-branches] OK"
|
||||||
58
deploy/deploy-by-script-to.sh
Executable file
58
deploy/deploy-by-script-to.sh
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy-by-script-to <target_branch>: checkout target, verify .secrets/<env>, force sync with origin, deploy target, checkout test.
|
||||||
|
# Launched from ia_dev (like other deploy scripts); applies to parent repo (../). Call after /change-to-all-branches (agent). Target: pprod | prod only.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
DEPLOY_IA="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
|
||||||
|
# Parent of ia_dev = project root (deploy applies to ../)
|
||||||
|
PROJECT_ROOT="$(cd "$DEPLOY_IA/../.." && pwd)"
|
||||||
|
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
|
||||||
|
cd "$PROJECT_ROOT" && exec "$SCRIPT_REAL" "$@"
|
||||||
|
fi
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "[deploy-by-script-to][ERROR] Not in a git repository (PROJECT_ROOT=${PROJECT_ROOT})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET_BRANCH="${1:-}"
|
||||||
|
if [[ -z "$TARGET_BRANCH" ]]; then
|
||||||
|
echo "[deploy-by-script-to][ERROR] Missing <target_branch> argument (expected: pprod | prod)" >&2
|
||||||
|
echo "Usage: ./deploy/deploy-by-script-to.sh <target_branch> (from ia_dev; pprod or prod only)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$TARGET_BRANCH" =~ ^(pprod|prod)$ ]]; then
|
||||||
|
echo "[deploy-by-script-to][ERROR] Invalid target branch: must be pprod or prod (got: '${TARGET_BRANCH}')" >&2
|
||||||
|
echo "Usage: ./deploy/deploy-by-script-to.sh <pprod|prod>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ "$current" != "test" ]]; then
|
||||||
|
echo "[deploy-by-script-to][ERROR] Must be on branch 'test' (current: '${current}'). Run /change-to-all-branches first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[deploy-by-script-to] Step 1/5: checkout ${TARGET_BRANCH}..."
|
||||||
|
if [[ "$(git rev-parse --abbrev-ref HEAD)" != "$TARGET_BRANCH" ]]; then
|
||||||
|
git checkout "$TARGET_BRANCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SECRETS_DIR="${PROJECT_ROOT}/.secrets/${TARGET_BRANCH}"
|
||||||
|
if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||||
|
echo "[deploy-by-script-to][ERROR] .secrets/${TARGET_BRANCH} does not exist at ${SECRETS_DIR}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[deploy-by-script-to] Step 2/5: .secrets/${TARGET_BRANCH} OK"
|
||||||
|
|
||||||
|
echo "[deploy-by-script-to] Step 3/5: force sync local branch with origin/${TARGET_BRANCH}..."
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard "origin/${TARGET_BRANCH}"
|
||||||
|
|
||||||
|
echo "[deploy-by-script-to] Step 4/5: deploy ${TARGET_BRANCH} (--import-v1 --skipSetupHost)..."
|
||||||
|
"$PROJECT_ROOT/deploy/scripts_v2/deploy.sh" "$TARGET_BRANCH" --import-v1 --skipSetupHost
|
||||||
|
|
||||||
|
echo "[deploy-by-script-to] Step 5/5: checkout test..."
|
||||||
|
git checkout test
|
||||||
|
|
||||||
|
echo "[deploy-by-script-to] OK: aligned, synced, deployed to ${TARGET_BRANCH}, back on test"
|
||||||
186
deploy/pousse.sh
Executable file
186
deploy/pousse.sh
Executable file
@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "[pousse][ERROR] Not in a git repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
|
||||||
|
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||||
|
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
|
||||||
|
SCRIPT_ABS="${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")"
|
||||||
|
cd "$PROJECT_ROOT" && exec "$SCRIPT_ABS" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve project id and config path: IA_PROJECT, .ia_project, or ai_project_id → projects/<id>/conf.json
|
||||||
|
# shellcheck source=../lib/project_config.sh
|
||||||
|
source "${IA_DEV_ROOT}/lib/project_config.sh"
|
||||||
|
|
||||||
|
remote="origin"
|
||||||
|
bump_version=false
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./deploy/pousse.sh [--remote <remote>] [--bump-version]
|
||||||
|
|
||||||
|
--bump-version Increment patch (third component) in VERSION before staging.
|
||||||
|
|
||||||
|
Reads a full multi-line commit message from STDIN, then:
|
||||||
|
- if not in repo root: re-exec from repo root (standardized execution)
|
||||||
|
- build check (npm run build in each directory listed in projects/<id>/conf.json build_dirs, if any; exit on failure)
|
||||||
|
- git add -A
|
||||||
|
- git commit -F <message>
|
||||||
|
- git push -u <remote> HEAD
|
||||||
|
|
||||||
|
The current branch must already exist on the remote (e.g. origin/<branch>); otherwise the script refuses to push.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
./deploy/pousse.sh <<'MSG'
|
||||||
|
Title
|
||||||
|
|
||||||
|
**Motivations:**
|
||||||
|
- ...
|
||||||
|
|
||||||
|
**Root causes:**
|
||||||
|
- ...
|
||||||
|
|
||||||
|
**Correctifs:**
|
||||||
|
- ...
|
||||||
|
|
||||||
|
**Evolutions:**
|
||||||
|
- ...
|
||||||
|
|
||||||
|
**Pages affectées:**
|
||||||
|
- ...
|
||||||
|
MSG
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--remote)
|
||||||
|
remote="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bump-version)
|
||||||
|
bump_version=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[pousse][ERROR] Unknown arg: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
||||||
|
echo "[pousse][ERROR] Detached HEAD is not supported" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
author_name="$(git config user.name || true)"
|
||||||
|
if [[ "$author_name" != "4NK" && "$author_name" != "Nicolas Cantu" ]]; then
|
||||||
|
echo "[pousse][ERROR] Refusing to commit: git user.name must be '4NK' or 'Nicolas Cantu' (got: '${author_name}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(git rev-parse --show-toplevel)"
|
||||||
|
# Build dirs from project config (projects/<id>/conf.json); skip if no config or no build_dirs
|
||||||
|
build_dirs=()
|
||||||
|
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r d; do
|
||||||
|
[[ -n "$d" ]] && build_dirs+=( "$d" )
|
||||||
|
done < <(jq -r '.build_dirs[]? // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [[ ${#build_dirs[@]} -gt 0 ]]; then
|
||||||
|
echo "[pousse] Build check (${#build_dirs[@]} dirs from project config)..."
|
||||||
|
for dir in "${build_dirs[@]}"; do
|
||||||
|
if [[ ! -d "${repo_root}/${dir}" ]]; then
|
||||||
|
echo "[pousse][WARN] Skipping build ${dir} (directory not found)" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "[pousse] Building ${dir}..."
|
||||||
|
(cd "${repo_root}/${dir}" && npm run build) || {
|
||||||
|
echo "[pousse][ERROR] Build failed in ${dir}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
echo "[pousse] Build check OK"
|
||||||
|
else
|
||||||
|
echo "[pousse] No build_dirs in project config (or no projects/<id>/conf.json / jq); skipping build check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_file="$(mktemp -t pousse-commit-msg.XXXXXX)"
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$msg_file"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
cat >"$msg_file" || true
|
||||||
|
if [[ ! -s "$msg_file" ]]; then
|
||||||
|
echo "[pousse][ERROR] Empty commit message on STDIN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$bump_version" == "true" ]]; then
|
||||||
|
version_file="${repo_root}/VERSION"
|
||||||
|
if [[ ! -f "$version_file" ]]; then
|
||||||
|
echo "[pousse][ERROR] VERSION not found at ${version_file}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
current="$(cat "$version_file" | sed 's/[[:space:]]//g')"
|
||||||
|
if ! [[ "$current" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "[pousse][ERROR] VERSION format must be X.Y.Z (got: '${current}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
maj="${current%%.*}"
|
||||||
|
min="${current#*.}"
|
||||||
|
min="${min%%.*}"
|
||||||
|
patch="${current##*.}"
|
||||||
|
patch=$((patch + 1))
|
||||||
|
new_version="${maj}.${min}.${patch}"
|
||||||
|
echo "$new_version" > "$version_file"
|
||||||
|
echo "[pousse] Bumped VERSION: ${current} -> ${new_version}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage all changes
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
git_status_short="$(git status -sb)"
|
||||||
|
echo "$git_status_short"
|
||||||
|
|
||||||
|
# Prevent committing potentially sensitive files
|
||||||
|
staged_files="$(git diff --cached --name-only || true)"
|
||||||
|
if [[ -n "$staged_files" ]]; then
|
||||||
|
if echo "$staged_files" | grep -Eiq '^(\.secrets/|\.env($|\.)|\.env\.|.*\.(key|pem|p12)$|.*credentials.*)'; then
|
||||||
|
echo "[pousse][ERROR] Refusing to commit: staged files look sensitive:" >&2
|
||||||
|
echo "$staged_files" | grep -Ei '^(\.secrets/|\.env($|\.)|\.env\.|.*\.(key|pem|p12)$|.*credentials.*)' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "[pousse] No staged changes to commit" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[pousse] Staged changes:"
|
||||||
|
git diff --cached --stat
|
||||||
|
|
||||||
|
git commit -F "$msg_file"
|
||||||
|
if ! git rev-parse "${remote}/${branch}" >/dev/null 2>&1; then
|
||||||
|
echo "[pousse][ERROR] Branch '${branch}' does not exist on remote '${remote}'. Refusing to push (would create a new remote branch). Create the branch on the remote first or push manually." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git push -u "$remote" HEAD
|
||||||
82
gitea-issues/agent-loop-chat-iterations.sh
Executable file
82
gitea-issues/agent-loop-chat-iterations.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bounded loop for chat: run mail retrieval N times, 1 minute between each.
|
||||||
|
# Use from Cursor chat when asked to "lance la boucle récupération emails puis attend 1 min et relance".
|
||||||
|
# Runs in foreground (no background); chat can run it for a few iterations to avoid timeout.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]
|
||||||
|
# N = number of iterations (default 3). Each iteration: mail-list-unread.sh then sleep 60.
|
||||||
|
# --repeat = after N iterations, relaunch (infinite loop of N-by-N runs).
|
||||||
|
# Output and mail list (expéditeur, sujet) are appended to projects/<id>/logs/gitea-issues/agent-loop-chat-iterations.log.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
REPEAT=""
|
||||||
|
if [ "${1:-}" = "--repeat" ]; then
|
||||||
|
REPEAT=1
|
||||||
|
N="${2:-3}"
|
||||||
|
else
|
||||||
|
N="${1:-3}"
|
||||||
|
fi
|
||||||
|
if ! [[ "$N" =~ ^[0-9]+$ ]] || [ "$N" -lt 1 ]; then
|
||||||
|
echo "Usage: $0 [N] [--repeat] — N positive integer (default 3). --repeat = relancer à la fin." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOG_DIR="${LOGS_GITEA}"
|
||||||
|
LOG_FILE="${LOG_DIR}/agent-loop-chat-iterations.log"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
# Ensure absolute path so logs are always in the same place
|
||||||
|
[[ "$LOG_FILE" != /* ]] && LOG_FILE="$(cd "$LOG_DIR" && pwd)/agent-loop-chat-iterations.log"
|
||||||
|
|
||||||
|
log_and_echo() {
|
||||||
|
echo "$1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log path and start so logs are never "empty" from path confusion
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — log file: $LOG_FILE"
|
||||||
|
# Test send at launch: one test email to nicolas.cantu@pm.me
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi vers nicolas.cantu@pm.me"
|
||||||
|
"${GITEA_ISSUES_DIR}/mail-send-reply.sh" --to "nicolas.cantu@pm.me" --subject "Test envoi - agent-loop-chat $(date +%Y-%m-%dT%H:%M:%S)" --body "Mail de test envoyé au lancement de agent-loop-chat-iterations.sh." 2>&1 | tee -a "$LOG_FILE"
|
||||||
|
if [ "${PIPESTATUS[0]:-0}" -eq 0 ]; then
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi OK"
|
||||||
|
else
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi échoué"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_iterations() {
|
||||||
|
for i in $(seq 1 "$N"); do
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — iteration $i/$N"
|
||||||
|
"${GITEA_ISSUES_DIR}/mail-list-unread.sh" 2>&1 | tee -a "$LOG_FILE" || true
|
||||||
|
if [ "$i" -lt "$N" ]; then
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — attente 60 s avant prochaine itération"
|
||||||
|
sleep 60
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — $N itérations terminées"
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
run_iterations
|
||||||
|
if [ -z "${REPEAT:-}" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
log_and_echo "[agent-loop-chat] $(date -Iseconds) — relance"
|
||||||
|
done
|
||||||
39
gitea-issues/agent-loop-lock-acquire.sh
Executable file
39
gitea-issues/agent-loop-lock-acquire.sh
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Try to acquire agent-loop.lock. Exit 0 and create lock if none or stale (>24h). Exit 1 if lock is recent.
|
||||||
|
# Run from repo root. Used by agent-loop agent before starting x cycles (section 2).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-lock-acquire.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
LOCK_FILE="${AGENT_LOOP_LOCK_FILE:-$LOGS_GITEA/agent-loop.lock}"
|
||||||
|
STALE_SEC=$((24 * 3600)) # 24 hours
|
||||||
|
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||||
|
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
mtime=$(stat -c %Y "$LOCK_FILE" 2>/dev/null) || mtime=0
|
||||||
|
now=$(date +%s)
|
||||||
|
if [ $((now - mtime)) -lt "$STALE_SEC" ]; then
|
||||||
|
echo "[agent-loop-lock-acquire] $(date -Iseconds) — Lock actif: $LOCK_FILE (mtime < 24 h). Ne pas lancer une deuxième instance." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s\n%s\n" "$$" "$(date -Iseconds)" > "$LOCK_FILE"
|
||||||
|
echo "[agent-loop-lock-acquire] $(date -Iseconds) — Lock acquis: $LOCK_FILE"
|
||||||
35
gitea-issues/agent-loop-lock-release.sh
Executable file
35
gitea-issues/agent-loop-lock-release.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Remove agent-loop.lock and optionally agent-loop.stop. Run at end of x cycles (normal or stop).
|
||||||
|
# Run from repo root.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-lock-release.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
LOCK_FILE="${AGENT_LOOP_LOCK_FILE:-$LOGS_GITEA/agent-loop.lock}"
|
||||||
|
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
|
||||||
|
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
rm -f "$LOCK_FILE"
|
||||||
|
echo "[agent-loop-lock-release] $(date -Iseconds) — Lock supprimé: $LOCK_FILE"
|
||||||
|
fi
|
||||||
|
if [ -f "$STOP_FILE" ]; then
|
||||||
|
rm -f "$STOP_FILE"
|
||||||
|
echo "[agent-loop-lock-release] $(date -Iseconds) — Fichier stop supprimé: $STOP_FILE"
|
||||||
|
fi
|
||||||
45
gitea-issues/agent-loop-retrieval-once.sh
Executable file
45
gitea-issues/agent-loop-retrieval-once.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-shot retrieval: run tickets-fetch-inbox (spooler from/to) then list-pending-spooler; write paths to agent-loop.pending and status file.
|
||||||
|
# Run from repo root. Used by agent-loop agent for "x times" cycles. Criterion: from/to in conf.json, not IMAP unread.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-retrieval-once.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
STATUS_FILE="${AGENT_LOOP_STATUS_FILE:-$LOGS_GITEA/agent-loop.status}"
|
||||||
|
PENDING_FILE="${AGENT_LOOP_PENDING_FILE:-$LOGS_GITEA/agent-loop.pending}"
|
||||||
|
mkdir -p "$(dirname "$STATUS_FILE")"
|
||||||
|
|
||||||
|
write_status() {
|
||||||
|
printf "%s\n%s\n%s\n" "$(date -Iseconds)" "$1" "${2:-}" > "$STATUS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
"${GITEA_ISSUES_DIR}/tickets-fetch-inbox.sh" 2>&1 || true
|
||||||
|
pending_out=""
|
||||||
|
pending_out=$("${GITEA_ISSUES_DIR}/list-pending-spooler.sh" 2>&1) || true
|
||||||
|
if [ -n "$pending_out" ] && echo "$pending_out" | grep -q "\.pending"; then
|
||||||
|
write_status "mails_pending" "One-shot: mails en attente dans le spooler (from/to)."
|
||||||
|
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$pending_out" > "$PENDING_FILE"
|
||||||
|
n=$(echo "$pending_out" | grep -c "\.pending" || true)
|
||||||
|
echo "[agent-loop-retrieval-once] $(date -Iseconds) — $n mail(s) en attente dans le spooler (critère from/to). Écrit dans agent-loop.pending"
|
||||||
|
else
|
||||||
|
write_status "idle" "One-shot: aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
|
||||||
|
[ -f "$PENDING_FILE" ] && : > "$PENDING_FILE"
|
||||||
|
echo "[agent-loop-retrieval-once] $(date -Iseconds) — Aucun mail en attente dans le spooler (critère: from/to dans conf.json)"
|
||||||
|
fi
|
||||||
27
gitea-issues/agent-loop-stop-requested.sh
Executable file
27
gitea-issues/agent-loop-stop-requested.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Exit 0 if agent-loop.stop exists (stop requested), 1 otherwise. Used at start of each cycle.
|
||||||
|
# Run from repo root.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop-requested.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
|
||||||
|
[ -f "$STOP_FILE" ]
|
||||||
|
# exit 0 if file exists, 1 otherwise
|
||||||
28
gitea-issues/agent-loop-stop.sh
Executable file
28
gitea-issues/agent-loop-stop.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Create agent-loop.stop so the running agent-loop (section 2) stops at the next cycle start.
|
||||||
|
# Run from repo root. Same paths as agent-loop-retrieval-once.sh (projects/<id>/logs/gitea-issues).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
|
||||||
|
mkdir -p "$(dirname "$STOP_FILE")"
|
||||||
|
touch "$STOP_FILE"
|
||||||
|
echo "[agent-loop-stop] $(date -Iseconds) — $STOP_FILE créé. La boucle en cours s'arrêtera au début du prochain cycle."
|
||||||
52
gitea-issues/agent-loop-treatment.sh
Executable file
52
gitea-issues/agent-loop-treatment.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Treatment loop: periodically check agent-loop.pending and run Cursor Agent CLI (gitea-issues-process workflow) when non-empty.
|
||||||
|
# Run from repo root. No timeout; runs forever. Do NOT start this script from the agent-loop agent (use bounded runs only).
|
||||||
|
#
|
||||||
|
# Usage (manual only; agent must not launch with nohup/&):
|
||||||
|
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-treatment.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
PENDING_FILE="${AGENT_LOOP_PENDING_FILE:-$LOGS_GITEA/agent-loop.pending}"
|
||||||
|
LOG_DIR="$(dirname "$PENDING_FILE")"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
AGENT_LOOP_ENV="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/agent-loop.env"
|
||||||
|
if [ -r "$AGENT_LOOP_ENV" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$AGENT_LOOP_ENV"
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
INTERVAL="${AGENT_LOOP_TREATMENT_INTERVAL_SEC:-60}"
|
||||||
|
AGENT_MODEL="${AGENT_LOOP_MODEL:-sonnet-4.6}"
|
||||||
|
|
||||||
|
PROMPT="Exécute le workflow mails du spooler (agent gitea-issues-process). Les chemins des fichiers .pending sont dans projects/<id>/logs/gitea-issues/agent-loop.pending (ou exécuter ./ia_dev/gitea-issues/list-pending-spooler.sh). Pour chaque fichier .pending : lire le JSON (from, to, subject, body, message_id, base). Rédiger une réponse pertinente (uniquement ton texte ; pas de citation — mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Envoyer avec ./ia_dev/gitea-issues/mail-send-reply.sh --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Après envoi réussi : ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Ne pas appeler mail-mark-read.sh (spooler)."
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if [ -s "$PENDING_FILE" ] && command -v agent >/dev/null 2>&1; then
|
||||||
|
echo "[agent-loop-treatment] $(date -Iseconds) — Pending non vide, lancement de l'agent Cursor."
|
||||||
|
if agent -p "$PROMPT" -f --model "$AGENT_MODEL" 2>&1; then
|
||||||
|
echo "[agent-loop-treatment] $(date -Iseconds) — Agent terminé."
|
||||||
|
else
|
||||||
|
echo "[agent-loop-treatment] $(date -Iseconds) — Agent terminé avec erreur."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
19
gitea-issues/agent-loop.env.example
Normal file
19
gitea-issues/agent-loop.env.example
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Agent-loop parameters (Cursor Agent CLI, model, interval).
|
||||||
|
# Copy to .secrets/gitea-issues/agent-loop.env and set as needed.
|
||||||
|
# Do not commit .secrets/gitea-issues/agent-loop.env (directory is gitignored).
|
||||||
|
#
|
||||||
|
# Run Cursor Agent when unread mails are detected (0 or 1)
|
||||||
|
# AGENT_LOOP_RUN_AGENT=1
|
||||||
|
#
|
||||||
|
# Model used by the CLI (default: sonnet-4.6 to avoid Opus usage limits)
|
||||||
|
# List: agent models
|
||||||
|
# AGENT_LOOP_MODEL=sonnet-4.6
|
||||||
|
#
|
||||||
|
# Polling interval in seconds (default: 60)
|
||||||
|
# AGENT_LOOP_INTERVAL_SEC=60
|
||||||
|
#
|
||||||
|
# Optional: custom paths for status, pending, lock and stop files
|
||||||
|
# AGENT_LOOP_STATUS_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.status
|
||||||
|
# AGENT_LOOP_PENDING_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.pending
|
||||||
|
# AGENT_LOOP_LOCK_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.lock
|
||||||
|
# AGENT_LOOP_STOP_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.stop
|
||||||
91
gitea-issues/agent-loop.sh
Executable file
91
gitea-issues/agent-loop.sh
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Agent loop: poll for mails to treat (spooler criterion: from/to in conf.json, not IMAP unread).
|
||||||
|
# Run from repo root. Runs forever. Do NOT start from the agent-loop agent (use bounded runs only, e.g. agent-loop-chat-iterations.sh N).
|
||||||
|
#
|
||||||
|
# Usage (manual only; agent must not launch with nohup/&):
|
||||||
|
# ./gitea-issues/agent-loop.sh [interval_seconds]
|
||||||
|
# AGENT_LOOP_INTERVAL_SEC=120 ./gitea-issues/agent-loop.sh
|
||||||
|
#
|
||||||
|
# Witness file: projects/<id>/logs/gitea-issues/agent-loop.status (or AGENT_LOOP_STATUS_FILE)
|
||||||
|
# State file (not a log): updated every iteration. If mtime is older than 2*interval, loop is considered stopped.
|
||||||
|
# Pending file: projects/<id>/logs/gitea-issues/agent-loop.pending (or AGENT_LOOP_PENDING_FILE)
|
||||||
|
# Written when there are .pending mails in the spooler (no matching .response); contains paths to treat.
|
||||||
|
#
|
||||||
|
# Optional: set AGENT_LOOP_RUN_AGENT=1 to run the Cursor Agent CLI when mails are detected.
|
||||||
|
# Requires Cursor Agent CLI (https://cursor.com/docs/cli/using). If "agent" is not in PATH, the loop only updates status/pending.
|
||||||
|
#
|
||||||
|
# Optional: AGENT_LOOP_MODEL=<model> to force the model (e.g. sonnet-4.6, gpt-5.4-low). Default: sonnet-4.6 to avoid Opus usage limits when running unattended.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
# Source user env so PATH includes ~/.local/bin (Cursor Agent CLI, etc.)
|
||||||
|
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$HOME/.bashrc" 2>/dev/null || true
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${ROOT}"
|
||||||
|
cd "$ROOT"
|
||||||
|
# Per-project logs under projects/<id>/logs (lib.sh sets PROJECT_LOGS_DIR)
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
|
||||||
|
|
||||||
|
# Load agent-loop parameters from .secrets (optional; .secrets under ia_dev)
|
||||||
|
AGENT_LOOP_ENV="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/agent-loop.env"
|
||||||
|
if [ -r "$AGENT_LOOP_ENV" ]; then
|
||||||
|
set +u
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$AGENT_LOOP_ENV"
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
INTERVAL="${1:-${AGENT_LOOP_INTERVAL_SEC:-60}}"
|
||||||
|
# STATUS_FILE: state/witness file (not a log) — indicates if the loop is active; under projects/<id>/logs/gitea-issues/ for per-project state.
|
||||||
|
STATUS_FILE="${AGENT_LOOP_STATUS_FILE:-$LOGS_GITEA/agent-loop.status}"
|
||||||
|
PENDING_FILE="${AGENT_LOOP_PENDING_FILE:-$LOGS_GITEA/agent-loop.pending}"
|
||||||
|
mkdir -p "$(dirname "$STATUS_FILE")"
|
||||||
|
|
||||||
|
write_status() {
|
||||||
|
local status="$1"
|
||||||
|
local detail="${2:-}"
|
||||||
|
printf "%s\n%s\n%s\n" "$(date -Iseconds)" "$status" "$detail" > "$STATUS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
write_status "running" "interval=${INTERVAL}s"
|
||||||
|
# Spooler flow: fetch by from/to (conf.json), then list .pending without .response. No IMAP unread criterion.
|
||||||
|
"${GITEA_ISSUES_DIR}/tickets-fetch-inbox.sh" 2>&1 || true
|
||||||
|
pending_out=""
|
||||||
|
pending_out=$("${GITEA_ISSUES_DIR}/list-pending-spooler.sh" 2>&1) || true
|
||||||
|
if [ -n "$pending_out" ] && echo "$pending_out" | grep -q "\.pending"; then
|
||||||
|
write_status "mails_pending" "Mails en attente dans le spooler (from/to). Lancer l'agent gitea-issues-process dans Cursor."
|
||||||
|
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$pending_out" > "$PENDING_FILE"
|
||||||
|
n=$(echo "$pending_out" | grep -c "\.pending" || true)
|
||||||
|
echo "[agent-loop] $(date -Iseconds) — $n mail(s) en attente dans le spooler (critère from/to). Lancer l'agent gitea-issues-process dans Cursor."
|
||||||
|
if [ "${AGENT_LOOP_RUN_AGENT:-0}" = "1" ] && command -v agent >/dev/null 2>&1; then
|
||||||
|
write_status "running_agent" "Lancement de l'agent Cursor pour traiter les mails du spooler."
|
||||||
|
echo "[agent-loop] $(date -Iseconds) — Lancement de l'agent Cursor (workflow gitea-issues-process spooler)."
|
||||||
|
AGENT_MODEL="${AGENT_LOOP_MODEL:-sonnet-4.6}"
|
||||||
|
echo "[agent-loop] $(date -Iseconds) — Modèle: $AGENT_MODEL"
|
||||||
|
AGENT_OPTS=(-p "Exécute le workflow mails du spooler (agent gitea-issues-process). 1) Lister les mails à traiter avec ./ia_dev/gitea-issues/list-pending-spooler.sh (ou utiliser les chemins dans agent-loop.pending). 2) Pour chaque fichier .pending : lire le JSON (from, to, subject, body, message_id, base). Rédiger une réponse pertinente (uniquement ton texte, pas de citation ; mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Envoyer avec ./ia_dev/gitea-issues/mail-send-reply.sh --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Après envoi réussi : ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Ne pas appeler mail-mark-read.sh (spooler)." -f --model "$AGENT_MODEL")
|
||||||
|
if agent "${AGENT_OPTS[@]}" 2>&1; then
|
||||||
|
write_status "agent_done" "Agent terminé."
|
||||||
|
else
|
||||||
|
write_status "mails_pending" "Agent terminé avec erreur ou interruption. Relancer l'agent manuellement si besoin."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
write_status "idle" "Aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
|
||||||
|
if [ -f "$PENDING_FILE" ]; then
|
||||||
|
: > "$PENDING_FILE"
|
||||||
|
fi
|
||||||
|
echo "[agent-loop] $(date -Iseconds) — Aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
|
||||||
|
fi
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
50
gitea-issues/comment-issue.sh
Executable file
50
gitea-issues/comment-issue.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Add a comment to an issue via Gitea API. The comment body is automatically
|
||||||
|
# signed with: Support IA du projet Lecoffre.io / ai.support.lecoffreio@4nkweb.com
|
||||||
|
# Usage: ./comment-issue.sh <issue_number> <message>
|
||||||
|
# Or: echo "message" | ./comment-issue.sh <issue_number> -
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
# Signature appended to every comment (same as mail replies)
|
||||||
|
COMMENT_SIGNATURE=$'\n\n--\nSupport IA du projet Lecoffre.io\nai.support.lecoffreio@4nkweb.com'
|
||||||
|
if [[ -n "${GITEA_COMMENT_SIGNATURE:-}" ]]; then
|
||||||
|
COMMENT_SIGNATURE="${GITEA_COMMENT_SIGNATURE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
log_err "Usage: $0 <issue_number> <message>"
|
||||||
|
log_err " Or: $0 <issue_number> - (read message from stdin)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISSUE_NUM="$1"
|
||||||
|
if [[ "${2:-}" == "-" ]]; then
|
||||||
|
BODY="$(cat)"
|
||||||
|
else
|
||||||
|
BODY="${2:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$BODY" ]]; then
|
||||||
|
log_err "Comment body is empty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="${BODY}${COMMENT_SIGNATURE}"
|
||||||
|
|
||||||
|
# Escape for JSON: jq -Rs . handles newlines and quotes
|
||||||
|
BODY_JSON="$(echo "$BODY" | jq -Rs .)"
|
||||||
|
RESPONSE="$(gitea_api_post "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}/comments" "{\"body\":${BODY_JSON}}")"
|
||||||
|
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
|
||||||
|
log_err "API error posting comment: ${RESPONSE:0:200}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Comment added to issue #${ISSUE_NUM}."
|
||||||
45
gitea-issues/create-branch-for-issue.sh
Executable file
45
gitea-issues/create-branch-for-issue.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Create a local branch for an issue. Branch name: issue/<number> (safe, short).
|
||||||
|
# Base branch defaults to "test"; ensure it is up to date (fetch + reset to origin/base).
|
||||||
|
# Usage: ./create-branch-for-issue.sh <issue_number> [base_branch]
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
log_err "Usage: $0 <issue_number> [base_branch]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISSUE_NUM="$1"
|
||||||
|
BASE="${2:-test}"
|
||||||
|
|
||||||
|
require_git_root || exit 1
|
||||||
|
|
||||||
|
if git show-ref --quiet "refs/heads/issue/${ISSUE_NUM}"; then
|
||||||
|
log_info "Branch issue/${ISSUE_NUM} already exists. Checking it out."
|
||||||
|
git checkout "issue/${ISSUE_NUM}"
|
||||||
|
echo "issue/${ISSUE_NUM}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git show-ref --quiet "refs/heads/${BASE}"; then
|
||||||
|
log_err "Base branch ${BASE} does not exist locally."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git fetch origin
|
||||||
|
if git show-ref --quiet "refs/remotes/origin/${BASE}"; then
|
||||||
|
git checkout "${BASE}"
|
||||||
|
git reset --hard "origin/${BASE}"
|
||||||
|
else
|
||||||
|
git checkout "${BASE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout -b "issue/${ISSUE_NUM}"
|
||||||
|
log_info "Created and checked out branch issue/${ISSUE_NUM} from ${BASE}."
|
||||||
|
echo "issue/${ISSUE_NUM}"
|
||||||
39
gitea-issues/get-issue.sh
Executable file
39
gitea-issues/get-issue.sh
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Get one issue by number. Output: JSON (default) or plain text summary (--summary).
|
||||||
|
# Usage: ./get-issue.sh <issue_number> [--summary]
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
log_err "Usage: $0 <issue_number> [--summary]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISSUE_NUM="$1"
|
||||||
|
SUMMARY=false
|
||||||
|
[[ "${2:-}" == "--summary" ]] && SUMMARY=true
|
||||||
|
|
||||||
|
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}")"
|
||||||
|
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
|
||||||
|
log_err "API error or invalid JSON (issue ${ISSUE_NUM}): ${RESPONSE:0:200}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SUMMARY" == true ]]; then
|
||||||
|
echo "--- Issue #${ISSUE_NUM} ---"
|
||||||
|
echo "Title: $(echo "$RESPONSE" | jq -r '.title')"
|
||||||
|
echo "State: $(echo "$RESPONSE" | jq -r '.state')"
|
||||||
|
echo "Labels: $(echo "$RESPONSE" | jq -r '[.labels[].name] | join(", ")')"
|
||||||
|
echo "Body:"
|
||||||
|
echo "$RESPONSE" | jq -r '.body // "(empty)"'
|
||||||
|
echo "---"
|
||||||
|
else
|
||||||
|
echo "$RESPONSE"
|
||||||
|
fi
|
||||||
43
gitea-issues/imap-bridge.env.example
Normal file
43
gitea-issues/imap-bridge.env.example
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# IMAP config for mail-to-issue (e.g. Proton Mail Bridge).
|
||||||
|
# Copy to .secrets/gitea-issues/imap-bridge.env and set real values.
|
||||||
|
# Do not commit .secrets/gitea-issues/imap-bridge.env (directory is gitignored).
|
||||||
|
#
|
||||||
|
# IMAP (read)
|
||||||
|
# IMAP_HOST=127.0.0.1
|
||||||
|
# IMAP_PORT=1143
|
||||||
|
# IMAP_USER=your-address@pm.me
|
||||||
|
# IMAP_PASSWORD=your-bridge-password
|
||||||
|
# IMAP_USE_STARTTLS=true
|
||||||
|
# For local Proton Bridge with self-signed cert, set to false to skip SSL verification (localhost only).
|
||||||
|
# IMAP_SSL_VERIFY=false
|
||||||
|
#
|
||||||
|
# SMTP (send replies; same Bridge account)
|
||||||
|
# SMTP_HOST=127.0.0.1
|
||||||
|
# SMTP_PORT=1025
|
||||||
|
# SMTP_USER=your-address@pm.me
|
||||||
|
# SMTP_PASSWORD=your-bridge-password
|
||||||
|
# SMTP_USE_STARTTLS=true
|
||||||
|
#
|
||||||
|
# Restrict listed mails to those sent to this address (default: ai.support.lecoffreio@4nkweb.com)
|
||||||
|
# MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com
|
||||||
|
#
|
||||||
|
# Only fetch/list mails on or after this date (IMAP format DD-Mon-YYYY). Default: 10-Mar-2026.
|
||||||
|
# MAIL_SINCE_DATE=10-Mar-2026
|
||||||
|
#
|
||||||
|
# Signature appended to every reply (default: Support IA du projet Lecoffre.io + ai.support.lecoffreio@4nkweb.com)
|
||||||
|
# MAIL_REPLY_SIGNATURE=--\\nSupport IA du projet Lecoffre.io\\nai.support.lecoffreio@4nkweb.com
|
||||||
|
#
|
||||||
|
# Signature for Gitea issue comments (optional; comment-issue.sh uses same default as mail signature)
|
||||||
|
# GITEA_COMMENT_SIGNATURE=
|
||||||
|
|
||||||
|
IMAP_HOST=127.0.0.1
|
||||||
|
IMAP_PORT=1143
|
||||||
|
IMAP_USER=
|
||||||
|
IMAP_PASSWORD=
|
||||||
|
IMAP_USE_STARTTLS=true
|
||||||
|
|
||||||
|
SMTP_HOST=127.0.0.1
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_USE_STARTTLS=true
|
||||||
122
gitea-issues/lib.sh
Executable file
122
gitea-issues/lib.sh
Executable file
@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Shared config and helpers for Gitea issues scripts.
|
||||||
|
# Source from gitea-issues/*.sh after cd to project root.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
GITEA_API_URL="${GITEA_API_URL:-https://git.4nkweb.com/api/v1}"
|
||||||
|
GITEA_REPO_OWNER="${GITEA_REPO_OWNER:-4nk}"
|
||||||
|
GITEA_REPO_NAME="${GITEA_REPO_NAME:-lecoffre_ng}"
|
||||||
|
|
||||||
|
# Optional: load project config from ia_dev (projects/<id>/conf.json); logs and data per project under projects/<id>/
|
||||||
|
PROJECT_CONFIG_PATH=""
|
||||||
|
PROJECT_LOGS_DIR=""
|
||||||
|
DATA_ISSUES_DIR=""
|
||||||
|
if [[ -f "${GITEA_ISSUES_DIR}/../lib/project_config.sh" ]]; then
|
||||||
|
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || true
|
||||||
|
if [[ -z "${PROJECT_ROOT:-}" || "$(basename "$PROJECT_ROOT")" = "ia_dev" ]]; then
|
||||||
|
PROJECT_ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
fi
|
||||||
|
IA_DEV_ROOT="$(cd "$GITEA_ISSUES_DIR/.." && pwd)"
|
||||||
|
if [[ -n "${PROJECT_ROOT:-}" ]]; then
|
||||||
|
# shellcheck source=../lib/project_config.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/../lib/project_config.sh"
|
||||||
|
if [[ -n "${PROJECT_SLUG:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
|
||||||
|
PROJECT_LOGS_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/logs"
|
||||||
|
DATA_ISSUES_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/data/issues"
|
||||||
|
mkdir -p "${PROJECT_LOGS_DIR}" "${DATA_ISSUES_DIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export PROJECT_LOGS_DIR
|
||||||
|
export DATA_ISSUES_DIR
|
||||||
|
|
||||||
|
# Load token: GITEA_TOKEN env, then project config git.token_file, then default .secrets path
|
||||||
|
load_gitea_token() {
|
||||||
|
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local token_file=""
|
||||||
|
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
|
||||||
|
local rel_path
|
||||||
|
rel_path="$(jq -r '.git.token_file // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
|
||||||
|
if [[ -n "$rel_path" && -n "${PROJECT_ROOT:-}" && -f "${PROJECT_ROOT}/${rel_path}" ]]; then
|
||||||
|
token_file="${PROJECT_ROOT}/${rel_path}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$token_file" ]]; then
|
||||||
|
token_file="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/token"
|
||||||
|
fi
|
||||||
|
if [[ -f "$token_file" ]]; then
|
||||||
|
GITEA_TOKEN="$(cat "$token_file")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "[gitea-issues] ERROR: GITEA_TOKEN not set and ${token_file} not found" >&2
|
||||||
|
echo "[gitea-issues] Set GITEA_TOKEN or create the token file with a Gitea Personal Access Token." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# curl wrapper for Gitea API (GET). Usage: gitea_api_get "/repos/owner/repo/issues"
|
||||||
|
gitea_api_get() {
|
||||||
|
local path="$1"
|
||||||
|
load_gitea_token || return 1
|
||||||
|
curl -sS -H "Accept: application/json" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITEA_API_URL}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# curl wrapper for Gitea API (POST). Usage: gitea_api_post "/repos/owner/repo/issues/123/comments" '{"body":"..."}'
|
||||||
|
gitea_api_post() {
|
||||||
|
local path="$1"
|
||||||
|
local data="${2:-}"
|
||||||
|
load_gitea_token || return 1
|
||||||
|
curl -sS -X POST -H "Accept: application/json" -H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-d "$data" \
|
||||||
|
"${GITEA_API_URL}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# curl wrapper for Gitea API (PATCH). Usage: gitea_api_patch "/repos/owner/repo/wiki/page/Foo" '{"content_base64":"..."}'
|
||||||
|
gitea_api_patch() {
|
||||||
|
local path="$1"
|
||||||
|
local data="${2:-}"
|
||||||
|
load_gitea_token || return 1
|
||||||
|
curl -sS -X PATCH -H "Accept: application/json" -H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-d "$data" \
|
||||||
|
"${GITEA_API_URL}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# curl wrapper for Gitea API (DELETE). Usage: gitea_api_delete "/repos/owner/repo/wiki/page/Foo"
|
||||||
|
gitea_api_delete() {
|
||||||
|
local path="$1"
|
||||||
|
load_gitea_token || return 1
|
||||||
|
curl -sS -X DELETE -H "Accept: application/json" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITEA_API_URL}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_ts() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
|
||||||
|
log_info() { echo "[$(log_ts)] [gitea-issues] $*"; }
|
||||||
|
log_err() { echo "[$(log_ts)] [gitea-issues] $*" >&2; }
|
||||||
|
|
||||||
|
# Require jq for JSON output
|
||||||
|
require_jq() {
|
||||||
|
if ! command -v jq &>/dev/null; then
|
||||||
|
log_err "jq is required. Install with: apt install jq / brew install jq"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure we are in the git repo root (for create-branch, etc.)
|
||||||
|
require_git_root() {
|
||||||
|
local root
|
||||||
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || true
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
log_err "Not inside a git repository."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
cd "$root"
|
||||||
|
}
|
||||||
35
gitea-issues/list-open-issues.sh
Executable file
35
gitea-issues/list-open-issues.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# List open issues for the configured Gitea repo.
|
||||||
|
# Output: JSON array (default) or one line per issue "number|title|state" with --lines.
|
||||||
|
# Usage: ./list-open-issues.sh [--lines] [--limit N]
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
LINES=false
|
||||||
|
LIMIT=50
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--lines) LINES=true; shift ;;
|
||||||
|
--limit) LIMIT="$2"; shift 2 ;;
|
||||||
|
*) log_err "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues?state=open&page=1&limit=${LIMIT}")"
|
||||||
|
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
|
||||||
|
log_err "API error or invalid JSON: ${RESPONSE:0:200}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$LINES" == true ]]; then
|
||||||
|
echo "$RESPONSE" | jq -r '.[] | "\(.number)|\(.title)|\(.state)"'
|
||||||
|
else
|
||||||
|
echo "$RESPONSE"
|
||||||
|
fi
|
||||||
23
gitea-issues/list-pending-spooler.sh
Executable file
23
gitea-issues/list-pending-spooler.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# List .pending files in projects/<id>/data/issues/ with status "pending" (one file per message; status updated in place).
|
||||||
|
# Run from repo root. Output: one path per line.
|
||||||
|
# Usage: ./ia_dev/gitea-issues/list-pending-spooler.sh
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
SPOOL="${DATA_ISSUES_DIR:-}"
|
||||||
|
if [[ -z "${SPOOL:-}" || ! -d "$SPOOL" ]]; then
|
||||||
|
echo "[gitea-issues] DATA_ISSUES_DIR not set or not a directory. Run from repo root with projects/<id> configured." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for f in "${SPOOL}"/*.pending; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
status="$(jq -r '.status // "pending"' "$f" 2>/dev/null)"
|
||||||
|
if [[ "$status" != "responded" ]]; then
|
||||||
|
echo "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
110
gitea-issues/mail-create-issue-from-email.py
Executable file
110
gitea-issues/mail-create-issue-from-email.py
Executable file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create one Gitea issue from one email (by UID), then mark the email as read.
|
||||||
|
If --title and/or --body are provided (formalized by agent), use them; else use subject and body from the email.
|
||||||
|
Usage: ./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..." ]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import email
|
||||||
|
import imaplib
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
from email.header import decode_header
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import (
|
||||||
|
create_gitea_issue,
|
||||||
|
load_gitea_config,
|
||||||
|
load_imap_config,
|
||||||
|
repo_root,
|
||||||
|
sanitize_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_header_value(header: str | None) -> str:
|
||||||
|
if not header:
|
||||||
|
return ""
|
||||||
|
from email.header import decode_header as dh
|
||||||
|
parts = dh(header)
|
||||||
|
result = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_body(msg: email.message.Message) -> str:
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
return ""
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if not payload:
|
||||||
|
return ""
|
||||||
|
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description="Create one Gitea issue from one email by UID")
|
||||||
|
ap.add_argument("--uid", required=True, help="IMAP message UID")
|
||||||
|
ap.add_argument("--title", default="", help="Formalized issue title (else use subject)")
|
||||||
|
ap.add_argument("--body", default="", help="Formalized issue body (else use email body + From)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
cfg = load_imap_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
gitea = load_gitea_config()
|
||||||
|
if not gitea["token"]:
|
||||||
|
print("[gitea-issues] ERROR: GITEA_TOKEN not set.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
mail.starttls(ssl.create_default_context())
|
||||||
|
mail.login(cfg["user"], cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
_, data = mail.fetch(args.uid, "(RFC822)")
|
||||||
|
if not data or not data[0]:
|
||||||
|
print("[gitea-issues] ERROR: Message UID not found.", file=sys.stderr)
|
||||||
|
mail.logout()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
msg = email.message_from_bytes(data[0][1])
|
||||||
|
from_ = decode_header_value(msg.get("From"))
|
||||||
|
subject = decode_header_value(msg.get("Subject"))
|
||||||
|
body_text = get_text_body(msg)
|
||||||
|
body_for_issue = f"**From:** {from_}\n\n{body_text}".strip()
|
||||||
|
|
||||||
|
title = args.title.strip() if args.title else sanitize_title(subject)
|
||||||
|
body = args.body.strip() if args.body else body_for_issue
|
||||||
|
|
||||||
|
issue = create_gitea_issue(title, body)
|
||||||
|
if not issue:
|
||||||
|
print("[gitea-issues] ERROR: Failed to create issue.", file=sys.stderr)
|
||||||
|
mail.logout()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mail.store(args.uid, "+FLAGS", "\\Seen")
|
||||||
|
mail.logout()
|
||||||
|
|
||||||
|
num = issue.get("number", "?")
|
||||||
|
print(f"[gitea-issues] Created issue #{num}: {title[:60]}")
|
||||||
|
print(f"ISSUE_NUMBER={num}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
gitea-issues/mail-create-issue-from-email.sh
Executable file
8
gitea-issues/mail-create-issue-from-email.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Create one Gitea issue from one email (by UID), mark email read. Run from repo root.
|
||||||
|
# Usage: ./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..." ]
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-create-issue-from-email.py" "$@"
|
||||||
208
gitea-issues/mail-get-thread.py
Normal file
208
gitea-issues/mail-get-thread.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fetch the full email thread (conversation) for a given message UID.
|
||||||
|
Uses Message-ID, References and In-Reply-To to find all messages in the thread.
|
||||||
|
Output format: same as mail-list-unread (--- MAIL UID=... --- ... --- END MAIL ---), chronological order.
|
||||||
|
Usage: mail-get-thread.py <uid>
|
||||||
|
or: ./gitea-issues/mail-get-thread.sh <uid>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import email
|
||||||
|
import imaplib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from email.header import decode_header
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import imap_since_date, load_imap_config, repo_root, imap_ssl_context
|
||||||
|
|
||||||
|
|
||||||
|
def decode_header_value(header: str | None) -> str:
|
||||||
|
if not header:
|
||||||
|
return ""
|
||||||
|
parts = decode_header(header)
|
||||||
|
result = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_body(msg: email.message.Message) -> str:
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
return payload.decode(
|
||||||
|
part.get_content_charset() or "utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if not payload:
|
||||||
|
return ""
|
||||||
|
return payload.decode(
|
||||||
|
msg.get_content_charset() or "utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_message_ids(refs: str | None, in_reply_to: str | None) -> set[str]:
|
||||||
|
"""Extract Message-ID values from References and In-Reply-To headers."""
|
||||||
|
ids: set[str] = set()
|
||||||
|
for raw in (refs or "", in_reply_to or ""):
|
||||||
|
for part in re.split(r"\s+", raw.strip()):
|
||||||
|
part = part.strip()
|
||||||
|
if part.startswith("<") and ">" in part:
|
||||||
|
ids.add(part)
|
||||||
|
elif part and "@" in part and part not in ("<", ">"):
|
||||||
|
ids.add(part if part.startswith("<") else f"<{part}>")
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def find_message_ids_from_msg(msg: email.message.Message) -> set[str]:
|
||||||
|
mid = (msg.get("Message-ID") or "").strip()
|
||||||
|
refs = (msg.get("References") or "").strip()
|
||||||
|
in_reply = (msg.get("In-Reply-To") or "").strip()
|
||||||
|
ids = {mid} if mid else set()
|
||||||
|
ids |= parse_message_ids(refs, in_reply)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_message_id(mail: imaplib.IMAP4, msg_id: str) -> list[str]:
|
||||||
|
"""Return list of UIDs (as strings) for messages with given Message-ID, on or after MAIL_SINCE_DATE."""
|
||||||
|
if not msg_id:
|
||||||
|
return []
|
||||||
|
if not msg_id.startswith("<"):
|
||||||
|
msg_id = f"<{msg_id}>"
|
||||||
|
if not msg_id.endswith(">"):
|
||||||
|
msg_id = msg_id + ">"
|
||||||
|
since = imap_since_date()
|
||||||
|
criterion = f'(HEADER Message-ID "{msg_id}" SINCE {since})'
|
||||||
|
try:
|
||||||
|
_, data = mail.search(None, criterion)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
if not data or not data[0]:
|
||||||
|
return []
|
||||||
|
return [u.decode("ascii") for u in data[0].split() if u]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_message_by_uid(
|
||||||
|
mail: imaplib.IMAP4, uid: str
|
||||||
|
) -> email.message.Message | None:
|
||||||
|
"""Fetch a single message by UID. Returns parsed email or None."""
|
||||||
|
try:
|
||||||
|
_, data = mail.fetch(uid.encode("ascii"), "(RFC822)")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not data or not data[0] or len(data[0]) < 2:
|
||||||
|
return None
|
||||||
|
raw = data[0][1]
|
||||||
|
if isinstance(raw, bytes):
|
||||||
|
return email.message_from_bytes(raw)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def format_message(uid: str, msg: email.message.Message) -> str:
|
||||||
|
mid = (msg.get("Message-ID") or "").strip()
|
||||||
|
from_ = decode_header_value(msg.get("From"))
|
||||||
|
to_ = decode_header_value(msg.get("To"))
|
||||||
|
subj = decode_header_value(msg.get("Subject"))
|
||||||
|
date_h = decode_header_value(msg.get("Date"))
|
||||||
|
body = get_text_body(msg)
|
||||||
|
lines = [
|
||||||
|
"--- MAIL",
|
||||||
|
f"UID={uid}",
|
||||||
|
"---",
|
||||||
|
"Message-ID: " + (mid or "(none)"),
|
||||||
|
"From: " + from_,
|
||||||
|
"To: " + (to_ or ""),
|
||||||
|
"Subject: " + subj,
|
||||||
|
"Date: " + (date_h or ""),
|
||||||
|
"Body:",
|
||||||
|
body or "(empty)",
|
||||||
|
"--- END MAIL ---",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: mail-get-thread.py <uid>", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
uid0 = sys.argv[1].strip()
|
||||||
|
if not uid0:
|
||||||
|
print("[gitea-issues] ERROR: UID required.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cfg = load_imap_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print(
|
||||||
|
"[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
|
||||||
|
mail.login(cfg["user"], cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
|
||||||
|
msg0 = fetch_message_by_uid(mail, uid0)
|
||||||
|
if not msg0:
|
||||||
|
print(f"[gitea-issues] No message found for UID={uid0}.", file=sys.stderr)
|
||||||
|
mail.logout()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
to_fetch: set[str] = find_message_ids_from_msg(msg0)
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
uids_by_mid: dict[str, str] = {}
|
||||||
|
|
||||||
|
while to_fetch:
|
||||||
|
mid = to_fetch.pop()
|
||||||
|
if not mid or mid in seen_ids:
|
||||||
|
continue
|
||||||
|
seen_ids.add(mid)
|
||||||
|
uids = search_by_message_id(mail, mid)
|
||||||
|
if uids:
|
||||||
|
uids_by_mid[mid] = uids[0]
|
||||||
|
msg = fetch_message_by_uid(mail, uids[0])
|
||||||
|
if msg:
|
||||||
|
to_fetch |= find_message_ids_from_msg(msg)
|
||||||
|
|
||||||
|
mid0 = (msg0.get("Message-ID") or "").strip()
|
||||||
|
if mid0 and mid0 not in uids_by_mid:
|
||||||
|
uids_by_mid[mid0] = uid0
|
||||||
|
|
||||||
|
collected: list[tuple[str, str, email.message.Message]] = []
|
||||||
|
for _mid, uid in uids_by_mid.items():
|
||||||
|
msg = fetch_message_by_uid(mail, uid)
|
||||||
|
if not msg:
|
||||||
|
continue
|
||||||
|
date_h = (msg.get("Date") or "").strip()
|
||||||
|
collected.append((date_h, uid, msg))
|
||||||
|
|
||||||
|
if uid0 not in uids_by_mid.values():
|
||||||
|
date0 = (msg0.get("Date") or "").strip()
|
||||||
|
collected.append((date0, uid0, msg0))
|
||||||
|
|
||||||
|
collected.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
for _date, uid, msg in collected:
|
||||||
|
print(format_message(uid, msg))
|
||||||
|
|
||||||
|
mail.logout()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
12
gitea-issues/mail-get-thread.sh
Executable file
12
gitea-issues/mail-get-thread.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetch full email thread for a given UID. Run from repo root.
|
||||||
|
# Usage: ./gitea-issues/mail-get-thread.sh <uid>
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: $0 <uid>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-get-thread.py" "$1"
|
||||||
118
gitea-issues/mail-list-unread.py
Executable file
118
gitea-issues/mail-list-unread.py
Executable file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
List unread emails via IMAP (e.g. Proton Mail Bridge). Read-only; does not mark as read.
|
||||||
|
Only lists messages sent to the configured alias (MAIL_FILTER_TO, default ai.support.lecoffreio@4nkweb.com).
|
||||||
|
Output is for the agent: each mail with UID, Message-ID, From, To, Subject, Date, body.
|
||||||
|
Usage: ./gitea-issues/mail-list-unread.sh
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import email
|
||||||
|
import imaplib
|
||||||
|
import sys
|
||||||
|
from email.header import decode_header
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add gitea-issues to path for mail_common
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import imap_search_criterion_unseen, load_imap_config, repo_root, imap_ssl_context
|
||||||
|
|
||||||
|
|
||||||
|
def decode_header_value(header: str | None) -> str:
|
||||||
|
if not header:
|
||||||
|
return ""
|
||||||
|
from email.header import decode_header as dh
|
||||||
|
parts = dh(header)
|
||||||
|
result = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_body(msg: email.message.Message) -> str:
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
return ""
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if not payload:
|
||||||
|
return ""
|
||||||
|
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def is_sent_to_alias(msg: email.message.Message, filter_to: str) -> bool:
|
||||||
|
"""True if any To/Delivered-To/X-Original-To/Cc header contains the filter address."""
|
||||||
|
if not filter_to:
|
||||||
|
return True
|
||||||
|
headers_to_check = ("To", "Delivered-To", "X-Original-To", "Cc", "Envelope-To")
|
||||||
|
for name in headers_to_check:
|
||||||
|
value = msg.get(name)
|
||||||
|
if value:
|
||||||
|
decoded = decode_header_value(value).lower()
|
||||||
|
if filter_to in decoded:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
cfg = load_imap_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
|
||||||
|
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
|
||||||
|
mail.login(cfg["user"], cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
criterion = imap_search_criterion_unseen()
|
||||||
|
_, nums = mail.search(None, criterion)
|
||||||
|
ids = nums[0].split()
|
||||||
|
if not ids:
|
||||||
|
print("[gitea-issues] No unread messages (IMAP UNSEEN, on or after MAIL_SINCE_DATE). For spooler criterion (from/to), use tickets-fetch-inbox.sh and list-pending-spooler.sh.")
|
||||||
|
mail.logout()
|
||||||
|
return
|
||||||
|
|
||||||
|
shown = 0
|
||||||
|
for uid in ids:
|
||||||
|
uid_s = uid.decode("ascii")
|
||||||
|
_, data = mail.fetch(uid, "(RFC822)")
|
||||||
|
if not data or not data[0]:
|
||||||
|
continue
|
||||||
|
msg = email.message_from_bytes(data[0][1])
|
||||||
|
if not is_sent_to_alias(msg, cfg.get("filter_to", "")):
|
||||||
|
continue
|
||||||
|
mid = msg.get("Message-ID", "").strip()
|
||||||
|
from_ = decode_header_value(msg.get("From"))
|
||||||
|
to_ = decode_header_value(msg.get("To"))
|
||||||
|
subj = decode_header_value(msg.get("Subject"))
|
||||||
|
date_h = decode_header_value(msg.get("Date"))
|
||||||
|
body = get_text_body(msg)
|
||||||
|
print("--- MAIL", f"UID={uid_s}", "---")
|
||||||
|
print("Message-ID:", mid or "(none)")
|
||||||
|
print("From:", from_)
|
||||||
|
print("To:", to_ or "")
|
||||||
|
print("Subject:", subj)
|
||||||
|
print("Date:", date_h or "")
|
||||||
|
print("Body:")
|
||||||
|
print(body or "(empty)")
|
||||||
|
print("--- END MAIL ---")
|
||||||
|
shown += 1
|
||||||
|
|
||||||
|
if shown == 0:
|
||||||
|
print("[gitea-issues] No unread messages sent to the configured alias (MAIL_FILTER_TO). For spooler (from/to in conf.json), use tickets-fetch-inbox.sh and list-pending-spooler.sh.")
|
||||||
|
mail.logout()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
gitea-issues/mail-list-unread.sh
Executable file
8
gitea-issues/mail-list-unread.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# List unread emails (read-only). Run from repo root.
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# REPO_ROOT = ia_dev so mail_common.repo_root() finds .secrets under ia_dev (independent of parent project)
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-list-unread.py"
|
||||||
41
gitea-issues/mail-mark-read.py
Executable file
41
gitea-issues/mail-mark-read.py
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Mark one email as read by UID (e.g. after replying without creating an issue).
|
||||||
|
Usage: ./gitea-issues/mail-mark-read.sh <uid>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import imaplib
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import load_imap_config, repo_root, imap_ssl_context
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("[gitea-issues] Usage: mail-mark-read.sh <uid>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
uid = sys.argv[1].strip()
|
||||||
|
|
||||||
|
cfg = load_imap_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
|
||||||
|
mail.login(cfg["user"], cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
mail.store(uid, "+FLAGS", "\\Seen")
|
||||||
|
mail.logout()
|
||||||
|
print("[gitea-issues] Marked as read.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
gitea-issues/mail-mark-read.sh
Executable file
8
gitea-issues/mail-mark-read.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Mark one email as read by UID. Run from repo root.
|
||||||
|
# Usage: ./gitea-issues/mail-mark-read.sh <uid>
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-mark-read.py" "$@"
|
||||||
108
gitea-issues/mail-send-reply.py
Executable file
108
gitea-issues/mail-send-reply.py
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Send a reply email via SMTP (e.g. Proton Mail Bridge).
|
||||||
|
Usage: ./gitea-issues/mail-send-reply.sh --to addr@example.com --subject "..." --body "..." [--in-reply-to "<msg-id>" [--references "<refs>"]]
|
||||||
|
Or: echo "body" | ./gitea-issues/mail-send-reply.sh --to addr@example.com --subject "..." [--in-reply-to "<msg-id>"]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
import sys
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import load_smtp_config, repo_root, imap_ssl_context
|
||||||
|
|
||||||
|
DEFAULT_SIGNATURE = """--
|
||||||
|
Support IA du projet Lecoffre.io
|
||||||
|
ai.support.lecoffreio@4nkweb.com"""
|
||||||
|
|
||||||
|
# Patterns that indicate the body contains a citation of the received message (forbidden).
|
||||||
|
CITATION_PATTERNS = (
|
||||||
|
"From:",
|
||||||
|
"Message-ID:",
|
||||||
|
"Message-ID :",
|
||||||
|
" wrote:",
|
||||||
|
" a écrit",
|
||||||
|
)
|
||||||
|
CITATION_LINE_START = (">",) # Quoted line start
|
||||||
|
|
||||||
|
|
||||||
|
def body_contains_citation(body: str) -> bool:
|
||||||
|
"""Return True if body looks like it contains the received message (citation)."""
|
||||||
|
if not body or not body.strip():
|
||||||
|
return False
|
||||||
|
lines = body.strip().splitlines()
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
for pat in CITATION_PATTERNS:
|
||||||
|
if pat in line:
|
||||||
|
return True
|
||||||
|
for start in CITATION_LINE_START:
|
||||||
|
if stripped.startswith(start):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_reply_signature() -> str:
|
||||||
|
sig = os.environ.get("MAIL_REPLY_SIGNATURE", "").strip()
|
||||||
|
if sig:
|
||||||
|
return "\n\n" + sig.replace("\\n", "\n")
|
||||||
|
return "\n\n" + DEFAULT_SIGNATURE
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description="Send reply email via Bridge SMTP")
|
||||||
|
ap.add_argument("--to", required=True, help="To address")
|
||||||
|
ap.add_argument("--subject", required=True, help="Subject")
|
||||||
|
ap.add_argument("--body", default="", help="Body (or use stdin)")
|
||||||
|
ap.add_argument("--in-reply-to", default="", help="Message-ID of the message we reply to")
|
||||||
|
ap.add_argument("--references", default="", help="References header for threading")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
cfg = load_smtp_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print("[gitea-issues] ERROR: SMTP_USER and SMTP_PASSWORD required.", file=sys.stderr)
|
||||||
|
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
body = args.body
|
||||||
|
if not body and not sys.stdin.isatty():
|
||||||
|
body = sys.stdin.read()
|
||||||
|
body = body.rstrip()
|
||||||
|
if body_contains_citation(body):
|
||||||
|
print(
|
||||||
|
"[gitea-issues] ERROR: Body must not contain the received message (no citation, no From:, Message-ID, wrote:, etc.). Send only your reply text.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
body = (body + get_reply_signature()).strip()
|
||||||
|
|
||||||
|
msg = MIMEText(body, "plain", "utf-8")
|
||||||
|
msg["Subject"] = args.subject
|
||||||
|
msg["From"] = cfg["user"]
|
||||||
|
msg["To"] = args.to
|
||||||
|
if args.in_reply_to:
|
||||||
|
msg["In-Reply-To"] = args.in_reply_to
|
||||||
|
if args.references:
|
||||||
|
msg["References"] = args.references
|
||||||
|
|
||||||
|
with smtplib.SMTP(cfg["host"], int(cfg["port"])) as smtp:
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
smtp.starttls(context=imap_ssl_context(cfg.get("ssl_verify", True)))
|
||||||
|
smtp.login(cfg["user"], cfg["password"])
|
||||||
|
smtp.sendmail(cfg["user"], [args.to], msg.as_string())
|
||||||
|
|
||||||
|
print("[gitea-issues] Reply sent.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
gitea-issues/mail-send-reply.sh
Executable file
8
gitea-issues/mail-send-reply.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Send reply email via Bridge SMTP. Run from repo root.
|
||||||
|
# Usage: ./gitea-issues/mail-send-reply.sh --to addr --subject "..." [--body "..." | stdin] [--in-reply-to "<msg-id>" [--references "..." ]]
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-send-reply.py" "$@"
|
||||||
333
gitea-issues/mail-thread-log.py
Normal file
333
gitea-issues/mail-thread-log.py
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Thread log: one file per email thread under projects/<id>/logs/gitea-issues/threads/.
|
||||||
|
Content: exchanges (received + sent), tickets (issues), commits.
|
||||||
|
Usage:
|
||||||
|
mail-thread-log.py get-id --uid <uid> # print THREAD_ID=...
|
||||||
|
mail-thread-log.py init --uid <uid> # create/update log from thread
|
||||||
|
mail-thread-log.py append-sent --thread-id <id> --to <addr> --subject "..." [--body "..."] [--date "..."]
|
||||||
|
mail-thread-log.py append-issue --thread-id <id> --issue <num> [--title "..."]
|
||||||
|
mail-thread-log.py append-commit --thread-id <id> --hash <hash> --message "..." [--branch "..."]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import load_gitea_config, load_imap_config, repo_root
|
||||||
|
from project_config import project_logs_dir
|
||||||
|
|
||||||
|
|
||||||
|
def threads_dir() -> Path:
|
||||||
|
"""Thread log directory: projects/<id>/logs/gitea-issues/threads/ or repo logs fallback."""
|
||||||
|
d = project_logs_dir() / "gitea-issues" / "threads"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_thread_id(raw: str, max_len: int = 80) -> str:
|
||||||
|
s = re.sub(r"[^a-zA-Z0-9._-]", "_", raw).strip("_")
|
||||||
|
return s[:max_len] if s else "thread_unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def get_thread_output(uid: str) -> str:
|
||||||
|
gitea_dir = Path(__file__).resolve().parent
|
||||||
|
root = gitea_dir.parent
|
||||||
|
env = {"GITEA_ISSUES_DIR": str(gitea_dir)}
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(gitea_dir / "mail-get-thread.py"), uid],
|
||||||
|
cwd=str(root),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env={**__import__("os").environ, **env},
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"mail-get-thread failed: {result.stderr or result.stdout or 'unknown'}"
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def parse_thread_blocks(text: str) -> list[dict[str, str]]:
|
||||||
|
"""Parse --- MAIL UID=... --- ... --- END MAIL --- blocks."""
|
||||||
|
blocks: list[dict[str, str]] = []
|
||||||
|
pattern = re.compile(
|
||||||
|
r"--- MAIL\s+UID=(\S+)\s+---\s*\n"
|
||||||
|
r"(?:Message-ID:\s*(.*?)\n)?"
|
||||||
|
r"From:\s*(.*?)\n"
|
||||||
|
r"To:\s*(.*?)\n"
|
||||||
|
r"Subject:\s*(.*?)\n"
|
||||||
|
r"Date:\s*(.*?)\n"
|
||||||
|
r"Body:\s*\n(.*?)--- END MAIL ---",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
for m in pattern.finditer(text):
|
||||||
|
blocks.append({
|
||||||
|
"uid": m.group(1).strip(),
|
||||||
|
"message_id": (m.group(2) or "").strip(),
|
||||||
|
"from": (m.group(3) or "").strip(),
|
||||||
|
"to": (m.group(4) or "").strip(),
|
||||||
|
"subject": (m.group(5) or "").strip(),
|
||||||
|
"date": (m.group(6) or "").strip(),
|
||||||
|
"body": (m.group(7) or "").strip(),
|
||||||
|
})
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def get_thread_id_from_uid(uid: str) -> str:
|
||||||
|
out = get_thread_output(uid)
|
||||||
|
blocks = parse_thread_blocks(out)
|
||||||
|
if not blocks:
|
||||||
|
return sanitize_thread_id(f"thread_uid_{uid}")
|
||||||
|
first_msg_id = (blocks[0].get("message_id") or "").strip() or blocks[0].get("uid", "")
|
||||||
|
return sanitize_thread_id(first_msg_id)
|
||||||
|
|
||||||
|
|
||||||
|
def format_exchange_received(block: dict[str, str]) -> str:
|
||||||
|
return (
|
||||||
|
f"### {block.get('date', '')} — Reçu\n"
|
||||||
|
f"- **De:** {block.get('from', '')}\n"
|
||||||
|
f"- **À:** {block.get('to', '')}\n"
|
||||||
|
f"- **Sujet:** {block.get('subject', '')}\n\n"
|
||||||
|
f"{block.get('body', '')}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_exchange_sent(block: dict[str, str]) -> str:
|
||||||
|
return (
|
||||||
|
f"### {block.get('date', '')} — Envoyé\n"
|
||||||
|
f"- **À:** {block.get('to', '')}\n"
|
||||||
|
f"- **Sujet:** {block.get('subject', '')}\n\n"
|
||||||
|
f"{block.get('body', '')}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_log(uid: str) -> str:
|
||||||
|
cfg = load_imap_config()
|
||||||
|
our_address = (cfg.get("filter_to") or "").strip().lower()
|
||||||
|
if not our_address:
|
||||||
|
our_address = (cfg.get("user") or "").strip().lower()
|
||||||
|
|
||||||
|
out = get_thread_output(uid)
|
||||||
|
blocks = parse_thread_blocks(out)
|
||||||
|
thread_id = get_thread_id_from_uid(uid)
|
||||||
|
log_path = threads_dir() / f"{thread_id}.md"
|
||||||
|
|
||||||
|
received_blocks: list[dict[str, str]] = []
|
||||||
|
sent_blocks: list[dict[str, str]] = []
|
||||||
|
for b in blocks:
|
||||||
|
from_ = (b.get("from") or "").lower()
|
||||||
|
if our_address and our_address in from_:
|
||||||
|
sent_blocks.append(b)
|
||||||
|
else:
|
||||||
|
received_blocks.append(b)
|
||||||
|
|
||||||
|
existing_tickets = ""
|
||||||
|
existing_commits = ""
|
||||||
|
if log_path.exists():
|
||||||
|
content = log_path.read_text(encoding="utf-8")
|
||||||
|
if "## Tickets (issues)" in content:
|
||||||
|
idx = content.index("## Tickets (issues)")
|
||||||
|
end = content.find("\n## ", idx + 1)
|
||||||
|
if end == -1:
|
||||||
|
end = len(content)
|
||||||
|
existing_tickets = content[idx:end].strip()
|
||||||
|
if "## Commits" in content:
|
||||||
|
idx = content.index("## Commits")
|
||||||
|
end = content.find("\n## ", idx + 1)
|
||||||
|
if end == -1:
|
||||||
|
end = len(content)
|
||||||
|
existing_commits = content[idx:end].strip()
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"# Fil — {thread_id}",
|
||||||
|
"",
|
||||||
|
"## Échanges reçus",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for b in received_blocks:
|
||||||
|
lines.append(format_exchange_received(b))
|
||||||
|
lines.append("## Échanges envoyés")
|
||||||
|
lines.append("")
|
||||||
|
for b in sent_blocks:
|
||||||
|
lines.append(format_exchange_sent(b))
|
||||||
|
if existing_tickets:
|
||||||
|
lines.append(existing_tickets)
|
||||||
|
lines.append("")
|
||||||
|
else:
|
||||||
|
lines.append("## Tickets (issues)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("(aucun)")
|
||||||
|
lines.append("")
|
||||||
|
if existing_commits:
|
||||||
|
lines.append(existing_commits)
|
||||||
|
lines.append("")
|
||||||
|
else:
|
||||||
|
lines.append("## Commits")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("(aucun)")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
log_path.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
return thread_id
|
||||||
|
|
||||||
|
|
||||||
|
def append_sent(
|
||||||
|
thread_id: str,
|
||||||
|
to_addr: str,
|
||||||
|
subject: str,
|
||||||
|
body: str = "",
|
||||||
|
date_str: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
if not date_str:
|
||||||
|
date_str = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||||
|
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
|
||||||
|
block = {
|
||||||
|
"date": date_str,
|
||||||
|
"to": to_addr,
|
||||||
|
"subject": subject,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
section = format_exchange_sent(block)
|
||||||
|
if not log_path.exists():
|
||||||
|
log_path.write_text(
|
||||||
|
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
|
||||||
|
"## Échanges envoyés\n\n" + section + "\n## Tickets (issues)\n\n(aucun)\n\n## Commits\n\n(aucun)\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
content = log_path.read_text(encoding="utf-8")
|
||||||
|
insert_marker = "## Échanges envoyés"
|
||||||
|
idx = content.find(insert_marker)
|
||||||
|
if idx == -1:
|
||||||
|
content += "\n\n## Échanges envoyés\n\n" + section
|
||||||
|
else:
|
||||||
|
next_section = content.find("\n## ", idx + 1)
|
||||||
|
if next_section == -1:
|
||||||
|
content = content.rstrip() + "\n\n" + section
|
||||||
|
else:
|
||||||
|
content = (
|
||||||
|
content[:next_section].rstrip() + "\n\n" + section + content[next_section:]
|
||||||
|
)
|
||||||
|
log_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def append_issue(thread_id: str, issue_num: str, title: str = "") -> None:
|
||||||
|
gitea = load_gitea_config()
|
||||||
|
base = f"{gitea['api_url'].replace('/api/v1', '')}/{gitea['owner']}/{gitea['repo']}/issues/{issue_num}"
|
||||||
|
line = f"- #{issue_num}" + (f" — {title}" if title else "") + f" — <{base}>\n"
|
||||||
|
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
|
||||||
|
if not log_path.exists():
|
||||||
|
log_path.write_text(
|
||||||
|
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
|
||||||
|
"## Échanges envoyés\n\n(aucun)\n\n## Tickets (issues)\n\n" + line + "\n## Commits\n\n(aucun)\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
content = log_path.read_text(encoding="utf-8")
|
||||||
|
marker = "## Tickets (issues)"
|
||||||
|
idx = content.find(marker)
|
||||||
|
if idx == -1:
|
||||||
|
content += "\n\n" + marker + "\n\n" + line
|
||||||
|
else:
|
||||||
|
end = idx + len(marker)
|
||||||
|
rest = content[end:]
|
||||||
|
if "(aucun)" in rest.split("\n## ")[0]:
|
||||||
|
content = content[:end] + "\n\n" + line + rest.replace("(aucun)\n", "", 1)
|
||||||
|
else:
|
||||||
|
content = content[:end] + "\n\n" + line + content[end:]
|
||||||
|
log_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def append_commit(
|
||||||
|
thread_id: str,
|
||||||
|
commit_hash: str,
|
||||||
|
message: str,
|
||||||
|
branch: str = "",
|
||||||
|
) -> None:
|
||||||
|
line = f"- `{commit_hash[:12]}`"
|
||||||
|
if branch:
|
||||||
|
line += f" ({branch})"
|
||||||
|
line += f" — {message.strip()}\n"
|
||||||
|
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
|
||||||
|
if not log_path.exists():
|
||||||
|
log_path.write_text(
|
||||||
|
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
|
||||||
|
"## Échanges envoyés\n\n(aucun)\n\n## Tickets (issues)\n\n(aucun)\n\n## Commits\n\n" + line,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
content = log_path.read_text(encoding="utf-8")
|
||||||
|
marker = "## Commits"
|
||||||
|
idx = content.find(marker)
|
||||||
|
if idx == -1:
|
||||||
|
content += "\n\n" + marker + "\n\n" + line
|
||||||
|
else:
|
||||||
|
end = idx + len(marker)
|
||||||
|
rest = content[end:]
|
||||||
|
if "(aucun)" in rest.split("\n## ")[0]:
|
||||||
|
content = content[:end] + "\n\n" + line + rest.replace("(aucun)\n", "", 1)
|
||||||
|
else:
|
||||||
|
content = content[:end] + "\n\n" + line + content[end:]
|
||||||
|
log_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(prog="mail-thread-log.py")
|
||||||
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||||
|
p_get = sub.add_parser("get-id")
|
||||||
|
p_get.add_argument("--uid", required=True, help="Mail UID")
|
||||||
|
p_init = sub.add_parser("init")
|
||||||
|
p_init.add_argument("--uid", required=True, help="Mail UID")
|
||||||
|
p_sent = sub.add_parser("append-sent")
|
||||||
|
p_sent.add_argument("--thread-id", required=True)
|
||||||
|
p_sent.add_argument("--to", required=True, dest="to_addr")
|
||||||
|
p_sent.add_argument("--subject", required=True)
|
||||||
|
p_sent.add_argument("--body", default="")
|
||||||
|
p_sent.add_argument("--date", default=None)
|
||||||
|
p_issue = sub.add_parser("append-issue")
|
||||||
|
p_issue.add_argument("--thread-id", required=True)
|
||||||
|
p_issue.add_argument("--issue", required=True)
|
||||||
|
p_issue.add_argument("--title", default="")
|
||||||
|
p_commit = sub.add_parser("append-commit")
|
||||||
|
p_commit.add_argument("--thread-id", required=True)
|
||||||
|
p_commit.add_argument("--hash", required=True)
|
||||||
|
p_commit.add_argument("--message", required=True)
|
||||||
|
p_commit.add_argument("--branch", default="")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
if args.cmd == "get-id":
|
||||||
|
tid = get_thread_id_from_uid(args.uid)
|
||||||
|
print(f"THREAD_ID={tid}")
|
||||||
|
return 0
|
||||||
|
if args.cmd == "init":
|
||||||
|
tid = init_log(args.uid)
|
||||||
|
print(f"THREAD_ID={tid}")
|
||||||
|
return 0
|
||||||
|
if args.cmd == "append-sent":
|
||||||
|
append_sent(
|
||||||
|
args.thread_id,
|
||||||
|
args.to_addr,
|
||||||
|
args.subject,
|
||||||
|
args.body,
|
||||||
|
args.date,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
if args.cmd == "append-issue":
|
||||||
|
append_issue(args.thread_id, args.issue, args.title)
|
||||||
|
return 0
|
||||||
|
if args.cmd == "append-commit":
|
||||||
|
append_commit(args.thread_id, args.hash, args.message, args.branch)
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
13
gitea-issues/mail-thread-log.sh
Executable file
13
gitea-issues/mail-thread-log.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Thread log: one file per thread under projects/<id>/logs/gitea-issues/threads/. Run from repo root.
|
||||||
|
# Usage:
|
||||||
|
# ./gitea-issues/mail-thread-log.sh get-id --uid <uid>
|
||||||
|
# ./gitea-issues/mail-thread-log.sh init --uid <uid>
|
||||||
|
# ./gitea-issues/mail-thread-log.sh append-sent --thread-id <id> --to <addr> --subject "..." [--body "..."] [--date "..."]
|
||||||
|
# ./gitea-issues/mail-thread-log.sh append-issue --thread-id <id> --issue <num> [--title "..."]
|
||||||
|
# ./gitea-issues/mail-thread-log.sh append-commit --thread-id <id> --hash <hash> --message "..." [--branch "..."]
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-thread-log.py" "$@"
|
||||||
116
gitea-issues/mail-to-issue.py
Executable file
116
gitea-issues/mail-to-issue.py
Executable file
@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create Gitea issues from unread emails via IMAP (e.g. Proton Mail Bridge).
|
||||||
|
|
||||||
|
**Preferred flow (agent-driven):** do not chain directly. Use mail-list-unread.sh
|
||||||
|
to list unread emails, then for each: formalize the issue or send a reply (mail-send-reply.sh);
|
||||||
|
only when a correction/evolution is ready, create the issue (mail-create-issue-from-email.sh
|
||||||
|
with optional formalized title/body), treat it (fix/evol), then comment on the issue and
|
||||||
|
reply to the email via the Bridge.
|
||||||
|
|
||||||
|
This script (mail-to-issue) is a **batch** fallback: it creates one issue per unread
|
||||||
|
message with title=subject and body=text+From, then marks messages as read. Use only
|
||||||
|
when the agent-driven flow is not used.
|
||||||
|
|
||||||
|
Reads IMAP config from .secrets/gitea-issues/imap-bridge.env (or env vars).
|
||||||
|
Reads Gitea token from GITEA_TOKEN or .secrets/gitea-issues/token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import email
|
||||||
|
import imaplib
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
from email.header import decode_header
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import (
|
||||||
|
create_gitea_issue,
|
||||||
|
imap_search_criterion_unseen,
|
||||||
|
load_gitea_config,
|
||||||
|
load_imap_config,
|
||||||
|
repo_root,
|
||||||
|
sanitize_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_header_value(header: str | None) -> str:
|
||||||
|
if not header:
|
||||||
|
return ""
|
||||||
|
parts = decode_header(header)
|
||||||
|
result = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_body(msg: email.message.Message) -> str:
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
return ""
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if not payload:
|
||||||
|
return ""
|
||||||
|
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
imap_cfg = load_imap_config()
|
||||||
|
if not imap_cfg["user"] or not imap_cfg["password"]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
gitea_cfg = load_gitea_config()
|
||||||
|
if not gitea_cfg["token"]:
|
||||||
|
print("[gitea-issues] ERROR: GITEA_TOKEN not set.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(imap_cfg["host"], int(imap_cfg["port"]))
|
||||||
|
if imap_cfg["use_starttls"]:
|
||||||
|
mail.starttls(ssl.create_default_context())
|
||||||
|
mail.login(imap_cfg["user"], imap_cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
criterion = imap_search_criterion_unseen()
|
||||||
|
_, nums = mail.search(None, criterion)
|
||||||
|
ids = nums[0].split()
|
||||||
|
if not ids:
|
||||||
|
print("[gitea-issues] No unread messages.")
|
||||||
|
mail.logout()
|
||||||
|
return
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
for uid in ids:
|
||||||
|
uid_s = uid.decode("ascii")
|
||||||
|
_, data = mail.fetch(uid, "(RFC822)")
|
||||||
|
if not data or not data[0]:
|
||||||
|
continue
|
||||||
|
msg = email.message_from_bytes(data[0][1])
|
||||||
|
subject = _decode_header_value(msg.get("Subject"))
|
||||||
|
from_ = _decode_header_value(msg.get("From"))
|
||||||
|
body_text = _get_text_body(msg)
|
||||||
|
body_for_issue = f"**From:** {from_}\n\n{body_text}".strip()
|
||||||
|
title = sanitize_title(subject)
|
||||||
|
issue = create_gitea_issue(title, body_for_issue)
|
||||||
|
if issue:
|
||||||
|
created += 1
|
||||||
|
print(f"[gitea-issues] Created issue #{issue.get('number', '?')}: {title[:60]}")
|
||||||
|
mail.store(uid_s, "+FLAGS", "\\Seen")
|
||||||
|
else:
|
||||||
|
print(f"[gitea-issues] Skipped (API failed): {title[:60]}", file=sys.stderr)
|
||||||
|
|
||||||
|
mail.logout()
|
||||||
|
print(f"[gitea-issues] Done. Created {created} issue(s).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
12
gitea-issues/mail-to-issue.sh
Executable file
12
gitea-issues/mail-to-issue.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Create Gitea issues from unread emails (IMAP). Requires Proton Mail Bridge
|
||||||
|
# or any IMAP server. Config: .secrets/gitea-issues/imap-bridge.env and token.
|
||||||
|
# Usage: ./gitea-issues/mail-to-issue.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/mail-to-issue.py"
|
||||||
144
gitea-issues/mail_common.py
Normal file
144
gitea-issues/mail_common.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# Shared config and helpers for gitea-issues mail scripts (IMAP/SMTP, Gitea).
|
||||||
|
# Used by mail-list-unread, mail-send-reply, mail-create-issue-from-email, mail-mark-read.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import ssl
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
# Only consider messages on or after this date (IMAP format DD-Mon-YYYY). Override with env MAIL_SINCE_DATE.
|
||||||
|
MAIL_SINCE_DATE_DEFAULT = "10-Mar-2026"
|
||||||
|
|
||||||
|
|
||||||
|
def imap_since_date() -> str:
|
||||||
|
"""Return IMAP SINCE date (DD-Mon-YYYY). Messages before this date are ignored by fetch/list scripts."""
|
||||||
|
return os.environ.get("MAIL_SINCE_DATE", MAIL_SINCE_DATE_DEFAULT).strip() or MAIL_SINCE_DATE_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def imap_search_criterion_all() -> str:
|
||||||
|
"""IMAP search: all messages on or after MAIL_SINCE_DATE."""
|
||||||
|
return f"SINCE {imap_since_date()}"
|
||||||
|
|
||||||
|
|
||||||
|
def imap_search_criterion_unseen() -> str:
|
||||||
|
"""IMAP search: unread messages on or after MAIL_SINCE_DATE."""
|
||||||
|
return f"(UNSEEN SINCE {imap_since_date()})"
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
# Root = directory containing gitea-issues (ia_dev). .secrets and logs live under ia_dev (./.secrets, ./logs)
|
||||||
|
# so they do not depend on the parent project; same ia_dev works for any project (../ai_project_id).
|
||||||
|
env_root = os.environ.get("REPO_ROOT")
|
||||||
|
if env_root:
|
||||||
|
return Path(env_root).resolve()
|
||||||
|
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
|
||||||
|
if issues_dir:
|
||||||
|
return Path(issues_dir).resolve().parent
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_env_file(path: Path) -> None:
|
||||||
|
if not path.is_file():
|
||||||
|
return
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip("'\"")
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def load_imap_config() -> dict[str, str]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
load_env_file(env_path)
|
||||||
|
ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", "true").lower()
|
||||||
|
ssl_verify = ssl_verify_raw not in ("0", "false", "no")
|
||||||
|
return {
|
||||||
|
"host": os.environ.get("IMAP_HOST", "127.0.0.1"),
|
||||||
|
"port": os.environ.get("IMAP_PORT", "1143"),
|
||||||
|
"user": os.environ.get("IMAP_USER", ""),
|
||||||
|
"password": os.environ.get("IMAP_PASSWORD", ""),
|
||||||
|
"use_starttls": os.environ.get("IMAP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"),
|
||||||
|
"ssl_verify": ssl_verify,
|
||||||
|
"filter_to": os.environ.get("MAIL_FILTER_TO", "ai.support.lecoffreio@4nkweb.com").strip().lower(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def imap_ssl_context(ssl_verify: bool = True) -> ssl.SSLContext:
|
||||||
|
"""Return SSL context for IMAP STARTTLS. Use ssl_verify=False only for local Bridge with self-signed cert."""
|
||||||
|
if ssl_verify:
|
||||||
|
return ssl.create_default_context()
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def load_smtp_config() -> dict[str, str]:
|
||||||
|
root = repo_root()
|
||||||
|
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
|
||||||
|
load_env_file(env_path)
|
||||||
|
ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", os.environ.get("SMTP_SSL_VERIFY", "true")).lower()
|
||||||
|
ssl_verify = ssl_verify_raw not in ("0", "false", "no")
|
||||||
|
return {
|
||||||
|
"host": os.environ.get("SMTP_HOST", os.environ.get("IMAP_HOST", "127.0.0.1")),
|
||||||
|
"port": os.environ.get("SMTP_PORT", "1025"),
|
||||||
|
"user": os.environ.get("SMTP_USER", os.environ.get("IMAP_USER", "")),
|
||||||
|
"password": os.environ.get("SMTP_PASSWORD", os.environ.get("IMAP_PASSWORD", "")),
|
||||||
|
"use_starttls": os.environ.get("SMTP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"),
|
||||||
|
"ssl_verify": ssl_verify,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_gitea_config() -> dict[str, str]:
|
||||||
|
root = repo_root()
|
||||||
|
token = os.environ.get("GITEA_TOKEN")
|
||||||
|
if not token:
|
||||||
|
token_path = root / ".secrets" / "gitea-issues" / "token"
|
||||||
|
if token_path.is_file():
|
||||||
|
token = token_path.read_text(encoding="utf-8").strip()
|
||||||
|
return {
|
||||||
|
"api_url": os.environ.get("GITEA_API_URL", "https://git.4nkweb.com/api/v1").rstrip("/"),
|
||||||
|
"owner": os.environ.get("GITEA_REPO_OWNER", "4nk"),
|
||||||
|
"repo": os.environ.get("GITEA_REPO_NAME", "lecoffre_ng"),
|
||||||
|
"token": token or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_title(raw: str, max_len: int = 200) -> str:
|
||||||
|
one_line = re.sub(r"\s+", " ", raw).strip()
|
||||||
|
return one_line[:max_len] if one_line else "(no subject)"
|
||||||
|
|
||||||
|
|
||||||
|
def create_gitea_issue(title: str, body: str) -> dict | None:
|
||||||
|
gitea = load_gitea_config()
|
||||||
|
if not gitea["token"]:
|
||||||
|
return None
|
||||||
|
url = f"{gitea['api_url']}/repos/{gitea['owner']}/{gitea['repo']}/issues"
|
||||||
|
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
|
||||||
|
req = Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
method="POST",
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"token {gitea['token']}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=30) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except (HTTPError, URLError):
|
||||||
|
return None
|
||||||
35
gitea-issues/print-issue-prompt.sh
Executable file
35
gitea-issues/print-issue-prompt.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Print issue number, title and body in a single block for agent consumption.
|
||||||
|
# Used by the gitea-issues-process agent to get the ticket content before calling /fix or /evol.
|
||||||
|
# Usage: ./print-issue-prompt.sh <issue_number>
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
log_err "Usage: $0 <issue_number>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISSUE_NUM="$1"
|
||||||
|
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}")"
|
||||||
|
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
|
||||||
|
log_err "API error (issue ${ISSUE_NUM}): ${RESPONSE:0:200}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TITLE="$(echo "$RESPONSE" | jq -r '.title')"
|
||||||
|
BODY="$(echo "$RESPONSE" | jq -r '.body // "(no description)"')"
|
||||||
|
LABELS="$(echo "$RESPONSE" | jq -r '[.labels[].name] | join(", ")')"
|
||||||
|
|
||||||
|
echo "Issue #${ISSUE_NUM}"
|
||||||
|
echo "Title: ${TITLE}"
|
||||||
|
echo "Labels: ${LABELS}"
|
||||||
|
echo ""
|
||||||
|
echo "${BODY}"
|
||||||
174
gitea-issues/project_config.py
Normal file
174
gitea-issues/project_config.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Load project config (projects/<id>/conf.json) for tickets spooler and authorized_emails.
|
||||||
|
# Requires PROJECT_ROOT (repo root with ai_project_id) and GITEA_ISSUES_DIR (ia_dev/gitea-issues).
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def project_root() -> Path:
|
||||||
|
"""Project repo root (parent of ia_dev). Where data/issues/ and ai_project_id live."""
|
||||||
|
env_root = os.environ.get("PROJECT_ROOT")
|
||||||
|
if env_root:
|
||||||
|
return Path(env_root).resolve()
|
||||||
|
env_repo = os.environ.get("REPO_ROOT")
|
||||||
|
if env_repo:
|
||||||
|
root = Path(env_repo).resolve()
|
||||||
|
# If REPO_ROOT is ia_dev, project root is parent
|
||||||
|
if (root / "gitea-issues").is_dir():
|
||||||
|
return root.parent
|
||||||
|
return root
|
||||||
|
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
|
||||||
|
if issues_dir:
|
||||||
|
return Path(issues_dir).resolve().parent.parent
|
||||||
|
return Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def ia_dev_root() -> Path:
|
||||||
|
"""Directory containing gitea-issues (ia_dev)."""
|
||||||
|
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
|
||||||
|
if issues_dir:
|
||||||
|
return Path(issues_dir).resolve().parent
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_project_config() -> dict | None:
|
||||||
|
"""Load projects/<slug>/conf.json. Returns None if not found or slug missing."""
|
||||||
|
root = project_root()
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
slug_path = root / "ai_project_id"
|
||||||
|
if not slug_path.is_file():
|
||||||
|
slug_path = root / ".ia_project"
|
||||||
|
slug = os.environ.get("IA_PROJECT", "").strip() if os.environ.get("IA_PROJECT") else None
|
||||||
|
if not slug and slug_path.is_file():
|
||||||
|
slug = slug_path.read_text(encoding="utf-8").strip()
|
||||||
|
if not slug:
|
||||||
|
return None
|
||||||
|
conf_path = ia_dev / "projects" / slug / "conf.json"
|
||||||
|
if not conf_path.is_file():
|
||||||
|
return None
|
||||||
|
with open(conf_path, encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def project_dir() -> Path | None:
|
||||||
|
"""Path to projects/<id>/ (under ia_dev). None if project config not found."""
|
||||||
|
root = project_root()
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
slug = os.environ.get("IA_PROJECT", "").strip()
|
||||||
|
if not slug and (root / "ai_project_id").is_file():
|
||||||
|
slug = (root / "ai_project_id").read_text(encoding="utf-8").strip()
|
||||||
|
if not slug and (root / ".ia_project").is_file():
|
||||||
|
slug = (root / ".ia_project").read_text(encoding="utf-8").strip()
|
||||||
|
if not slug:
|
||||||
|
return None
|
||||||
|
return ia_dev / "projects" / slug
|
||||||
|
|
||||||
|
|
||||||
|
def data_issues_dir() -> Path:
|
||||||
|
"""Path to data/issues/ spooler under projects/<id>/ (ia_dev/projects/<id>/data/issues)."""
|
||||||
|
pd = project_dir()
|
||||||
|
if pd is not None:
|
||||||
|
return pd / "data" / "issues"
|
||||||
|
return project_root() / "data" / "issues"
|
||||||
|
|
||||||
|
|
||||||
|
def data_issues_dir_for_project(project_id: str) -> Path:
|
||||||
|
"""Path to data/issues/ for a given project id (ia_dev/projects/<id>/data/issues)."""
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
return ia_dev / "projects" / project_id / "data" / "issues"
|
||||||
|
|
||||||
|
|
||||||
|
def project_logs_dir() -> Path:
|
||||||
|
"""Path to logs/ under projects/<id>/ (ia_dev/projects/<id>/logs)."""
|
||||||
|
pd = project_dir()
|
||||||
|
if pd is not None:
|
||||||
|
return pd / "logs"
|
||||||
|
return project_root() / "logs"
|
||||||
|
|
||||||
|
|
||||||
|
def authorized_emails() -> dict[str, str | list[str]]:
|
||||||
|
"""Return tickets.authorized_emails (to, from list). Empty dict if missing."""
|
||||||
|
conf = load_project_config()
|
||||||
|
if not conf:
|
||||||
|
return {}
|
||||||
|
tickets = conf.get("tickets") or {}
|
||||||
|
return tickets.get("authorized_emails") or {}
|
||||||
|
|
||||||
|
|
||||||
|
def list_project_ids() -> list[str]:
|
||||||
|
"""List all project ids (directory names under projects/)."""
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
projects_dir = ia_dev / "projects"
|
||||||
|
if not projects_dir.is_dir():
|
||||||
|
return []
|
||||||
|
return [d.name for d in projects_dir.iterdir() if d.is_dir() and (d / "conf.json").is_file()]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_project_id_by_email_to(to_address: str) -> str | None:
|
||||||
|
"""Find project id whose tickets.authorized_emails.to matches the given address (case-insensitive)."""
|
||||||
|
if not to_address or not to_address.strip():
|
||||||
|
return None
|
||||||
|
to_normalized = to_address.strip().lower()
|
||||||
|
for pid in list_project_ids():
|
||||||
|
conf_path = ia_dev_root() / "projects" / pid / "conf.json"
|
||||||
|
try:
|
||||||
|
with open(conf_path, encoding="utf-8") as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
tickets = conf.get("tickets") or {}
|
||||||
|
auth = tickets.get("authorized_emails") or {}
|
||||||
|
conf_to = (auth.get("to") or "").strip().lower()
|
||||||
|
if conf_to == to_normalized:
|
||||||
|
return pid
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _list_project_envs(project_id: str) -> list[str]:
|
||||||
|
"""List env names (subdirs of .secrets) for a project that contain ia_token."""
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
secrets_dir = ia_dev / "projects" / project_id / ".secrets"
|
||||||
|
if not secrets_dir.is_dir():
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
d.name
|
||||||
|
for d in secrets_dir.iterdir()
|
||||||
|
if d.is_dir() and (d / "ia_token").is_file()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_project_and_env_by_token(token: str) -> tuple[str, str] | None:
|
||||||
|
"""Find (project_id, env) by scanning projects/<id>/.secrets/<env>/ia_token. Returns first match."""
|
||||||
|
if not token or not token.strip():
|
||||||
|
return None
|
||||||
|
token_stripped = token.strip()
|
||||||
|
for pid in list_project_ids():
|
||||||
|
for env in _list_project_envs(pid):
|
||||||
|
token_path = ia_dev_root() / "projects" / pid / ".secrets" / env / "ia_token"
|
||||||
|
try:
|
||||||
|
content = token_path.read_text(encoding="utf-8").strip()
|
||||||
|
# Token is either full value in file or base + env (e.g. nicolecoffreio<env>)
|
||||||
|
if content == token_stripped or (content + env) == token_stripped:
|
||||||
|
return (pid, env)
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_project_id_by_token(token: str) -> str | None:
|
||||||
|
"""Find project id whose .secrets/<env>/ia_token matches the given token."""
|
||||||
|
resolved = resolve_project_and_env_by_token(token)
|
||||||
|
return resolved[0] if resolved else None
|
||||||
|
|
||||||
|
|
||||||
|
def load_project_config_by_id(project_id: str) -> dict | None:
|
||||||
|
"""Load conf.json for a given project id. Returns None if not found."""
|
||||||
|
ia_dev = ia_dev_root()
|
||||||
|
conf_path = ia_dev / "projects" / project_id / "conf.json"
|
||||||
|
if not conf_path.is_file():
|
||||||
|
return None
|
||||||
|
with open(conf_path, encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
321
gitea-issues/tickets-fetch-inbox.py
Normal file
321
gitea-issues/tickets-fetch-inbox.py
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fetch inbox emails and route each message to the project whose tickets.authorized_emails.to matches the message To.
|
||||||
|
|
||||||
|
Project is resolved per message: To/Delivered-To/X-Original-To are compared to tickets.authorized_emails.to in each
|
||||||
|
projects/<id>/conf.json; the first matching project id is used. Only messages from authorized_emails.from are kept.
|
||||||
|
Messages on or after MAIL_SINCE_DATE are considered. Does not use UNSEEN; does not mark as read (BODY.PEEK[]).
|
||||||
|
Writes to projects/<id>/data/issues/ as JSON <date>.<msg_id>.<from>.pending. One file per message.
|
||||||
|
|
||||||
|
State: we skip creating .pending if .pending exists or .response exists for that base.
|
||||||
|
Usage: run with GITEA_ISSUES_DIR set (e.g. via tickets-fetch-inbox.sh). MAIL_SINCE_DATE overrides date (DD-Mon-YYYY).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import email
|
||||||
|
import hashlib
|
||||||
|
import imaplib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from email.header import decode_header
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from mail_common import imap_search_criterion_all, load_imap_config, imap_ssl_context
|
||||||
|
from project_config import (
|
||||||
|
data_issues_dir_for_project,
|
||||||
|
ia_dev_root,
|
||||||
|
load_project_config_by_id,
|
||||||
|
project_root,
|
||||||
|
resolve_project_id_by_email_to,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_header_value(header: str | None) -> str:
|
||||||
|
if not header:
|
||||||
|
return ""
|
||||||
|
parts = decode_header(header)
|
||||||
|
result = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_from_address(from_header: str) -> str:
|
||||||
|
"""Extract email address from From header (e.g. 'Name <user@host>' -> user@host)."""
|
||||||
|
if not from_header:
|
||||||
|
return ""
|
||||||
|
match = re.search(r"<([^>]+)>", from_header)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip().lower()
|
||||||
|
return from_header.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_body(msg: email.message.Message) -> str:
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
return payload.decode(
|
||||||
|
part.get_content_charset() or "utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if not payload:
|
||||||
|
return ""
|
||||||
|
return payload.decode(
|
||||||
|
msg.get_content_charset() or "utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_addresses(header_value: str) -> set[str]:
|
||||||
|
"""Extract email addresses from a header value (e.g. 'Name <user@host>, other@host')."""
|
||||||
|
if not header_value or not header_value.strip():
|
||||||
|
return set()
|
||||||
|
decoded = decode_header_value(header_value).strip()
|
||||||
|
# Angle-bracket: <...@...>
|
||||||
|
in_angle = re.findall(r"<([^>]+)>", decoded)
|
||||||
|
# Standalone addr-spec (simplified: local@domain)
|
||||||
|
plain = re.findall(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]", decoded)
|
||||||
|
out: set[str] = set()
|
||||||
|
for a in in_angle:
|
||||||
|
out.add(a.strip().lower())
|
||||||
|
for a in plain:
|
||||||
|
out.add(a.strip().lower())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_message_to_addresses(msg: email.message.Message) -> list[str]:
|
||||||
|
"""Return ordered list of recipient addresses (To, Delivered-To, X-Original-To, etc.) for project resolution."""
|
||||||
|
order = ("To", "Delivered-To", "X-Original-To", "X-Delivered-To", "X-Envelope-To", "Envelope-To")
|
||||||
|
seen: set[str] = set()
|
||||||
|
result: list[str] = []
|
||||||
|
for name in order:
|
||||||
|
value = msg.get(name)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
addrs = _extract_addresses(decode_header_value(value))
|
||||||
|
for a in addrs:
|
||||||
|
if a not in seen:
|
||||||
|
seen.add(a)
|
||||||
|
result.append(a)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_from_for_filename(email_addr: str) -> str:
|
||||||
|
"""Filesystem-safe string from email (e.g. user@example.com -> user_example.com)."""
|
||||||
|
return re.sub(r"[^a-zA-Z0-9._-]", "_", email_addr.replace("@", "_"))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_message_id(mid: str | None, uid_s: str, date_str: str, from_addr: str) -> str:
|
||||||
|
"""Deterministic 8-char id so the same message always gets the same base filename."""
|
||||||
|
raw = mid or f"{uid_s}_{date_str}_{from_addr}"
|
||||||
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_attachment_filename(name: str) -> str:
|
||||||
|
"""Safe filename for attachment (no path, no dangerous chars)."""
|
||||||
|
if not name or not name.strip():
|
||||||
|
return "attachment"
|
||||||
|
base = Path(name).name
|
||||||
|
return re.sub(r"[^a-zA-Z0-9._-]", "_", base)[:200] or "attachment"
|
||||||
|
|
||||||
|
|
||||||
|
def get_attachments(msg: email.message.Message) -> list[tuple[str, bytes, str]]:
|
||||||
|
"""Return list of (filename, payload_bytes, content_type) for each attachment."""
|
||||||
|
result: list[tuple[str, bytes, str]] = []
|
||||||
|
for part in msg.walk():
|
||||||
|
content_type = (part.get_content_type() or "").lower()
|
||||||
|
if content_type.startswith("multipart/"):
|
||||||
|
continue
|
||||||
|
filename = part.get_filename()
|
||||||
|
if not filename:
|
||||||
|
# Optional: treat inline images etc. with Content-Disposition attachment
|
||||||
|
disp = part.get("Content-Disposition") or ""
|
||||||
|
if "attachment" in disp.lower():
|
||||||
|
ext = ""
|
||||||
|
if "image" in content_type:
|
||||||
|
ext = ".bin" if "octet-stream" in content_type else ".img"
|
||||||
|
filename = f"attachment{ext}"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
filename = decode_header_value(filename).strip()
|
||||||
|
if not filename:
|
||||||
|
continue
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload is None:
|
||||||
|
continue
|
||||||
|
result.append((filename, payload, content_type))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_references(refs: str | None) -> list[str]:
|
||||||
|
if not refs:
|
||||||
|
return []
|
||||||
|
return [x.strip() for x in re.split(r"\s+", refs) if x.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
cfg = load_imap_config()
|
||||||
|
if not cfg["user"] or not cfg["password"]:
|
||||||
|
print("[tickets-fetch-inbox] IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Spool is per-project; each message is routed by its To address to projects/<id>/data/issues/
|
||||||
|
print("[tickets-fetch-inbox] Project resolved per message from To/Delivered-To/X-Original-To (tickets.authorized_emails.to).")
|
||||||
|
|
||||||
|
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
|
||||||
|
if cfg["use_starttls"]:
|
||||||
|
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
|
||||||
|
mail.login(cfg["user"], cfg["password"])
|
||||||
|
mail.select("INBOX")
|
||||||
|
# Do not use UNSEEN; fetch messages on or after MAIL_SINCE_DATE (default 10-Mar-2026). Filter by authorized senders only.
|
||||||
|
# Use BODY.PEEK[] instead of RFC822 so the server does not set \Seen (emails stay "unread").
|
||||||
|
since_criterion = imap_search_criterion_all()
|
||||||
|
_, nums = mail.search(None, since_criterion)
|
||||||
|
ids = nums[0].split()
|
||||||
|
written = 0
|
||||||
|
skipped_fetch = 0
|
||||||
|
skipped_no_project = 0
|
||||||
|
skipped_from = 0
|
||||||
|
skipped_pending = 0
|
||||||
|
skipped_response = 0
|
||||||
|
for uid in ids:
|
||||||
|
uid_s = uid.decode("ascii")
|
||||||
|
_, data = mail.fetch(uid, "(BODY.PEEK[])")
|
||||||
|
if not data or not data[0]:
|
||||||
|
skipped_fetch += 1
|
||||||
|
continue
|
||||||
|
raw = data[0]
|
||||||
|
raw_bytes = None
|
||||||
|
if isinstance(raw, tuple):
|
||||||
|
if len(raw) >= 2 and isinstance(raw[1], bytes):
|
||||||
|
raw_bytes = raw[1]
|
||||||
|
elif len(raw) >= 2 and isinstance(raw[1], str):
|
||||||
|
raw_bytes = raw[1].encode("utf-8", errors="replace")
|
||||||
|
elif isinstance(raw, bytes):
|
||||||
|
raw_bytes = raw
|
||||||
|
if not raw_bytes:
|
||||||
|
skipped_fetch += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = email.message_from_bytes(raw_bytes)
|
||||||
|
except Exception:
|
||||||
|
skipped_fetch += 1
|
||||||
|
continue
|
||||||
|
to_addresses = get_message_to_addresses(msg)
|
||||||
|
project_id: str | None = None
|
||||||
|
for addr in to_addresses:
|
||||||
|
project_id = resolve_project_id_by_email_to(addr)
|
||||||
|
if project_id:
|
||||||
|
break
|
||||||
|
if not project_id:
|
||||||
|
skipped_no_project += 1
|
||||||
|
continue
|
||||||
|
conf = load_project_config_by_id(project_id)
|
||||||
|
if not conf:
|
||||||
|
skipped_no_project += 1
|
||||||
|
continue
|
||||||
|
auth = (conf.get("tickets") or {}).get("authorized_emails") or {}
|
||||||
|
from_list = auth.get("from")
|
||||||
|
if isinstance(from_list, list):
|
||||||
|
allowed_from = {str(a).strip().lower() for a in from_list if a}
|
||||||
|
elif isinstance(from_list, str):
|
||||||
|
allowed_from = {a.strip().lower() for a in re.split(r"[,;]", from_list) if a.strip()}
|
||||||
|
else:
|
||||||
|
allowed_from = set()
|
||||||
|
from_raw = decode_header_value(msg.get("From"))
|
||||||
|
from_addr = parse_from_address(from_raw)
|
||||||
|
if from_addr not in allowed_from:
|
||||||
|
skipped_from += 1
|
||||||
|
continue
|
||||||
|
spool = data_issues_dir_for_project(project_id)
|
||||||
|
spool.mkdir(parents=True, exist_ok=True)
|
||||||
|
mid = (msg.get("Message-ID") or "").strip()
|
||||||
|
to_raw = decode_header_value(msg.get("To"))
|
||||||
|
to_addrs = [a.strip() for a in re.split(r"[,;]", to_raw) if a.strip()]
|
||||||
|
subj = decode_header_value(msg.get("Subject"))
|
||||||
|
date_h = decode_header_value(msg.get("Date"))
|
||||||
|
refs = parse_references(msg.get("References"))
|
||||||
|
in_reply_to = (msg.get("In-Reply-To") or "").strip() or None
|
||||||
|
body = get_text_body(msg)
|
||||||
|
try:
|
||||||
|
if date_h:
|
||||||
|
dt = parsedate_to_datetime(date_h)
|
||||||
|
date_str = dt.strftime("%Y-%m-%dT%H%M%S")
|
||||||
|
else:
|
||||||
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
|
||||||
|
except Exception:
|
||||||
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
|
||||||
|
from_safe = sanitize_from_for_filename(from_addr)
|
||||||
|
msg_id_short = generate_message_id(mid, uid_s, date_str, from_addr)
|
||||||
|
base = f"{date_str}.{msg_id_short}.{from_safe}"
|
||||||
|
path = spool / f"{base}.pending"
|
||||||
|
if path.exists():
|
||||||
|
skipped_pending += 1
|
||||||
|
continue
|
||||||
|
# Already treated: .response exists (we don't keep .pending after replying).
|
||||||
|
if (spool / f"{base}.response").exists():
|
||||||
|
skipped_response += 1
|
||||||
|
continue
|
||||||
|
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
attachments_meta: list[dict[str, str | int]] = []
|
||||||
|
attachment_parts = get_attachments(msg)
|
||||||
|
if attachment_parts:
|
||||||
|
att_dir = spool / f"{base}.d"
|
||||||
|
att_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for idx, (orig_name, payload_bytes, content_type) in enumerate(attachment_parts):
|
||||||
|
safe_name = sanitize_attachment_filename(orig_name)
|
||||||
|
stored_name = f"{idx}_{safe_name}"
|
||||||
|
stored_path = att_dir / stored_name
|
||||||
|
stored_path.write_bytes(payload_bytes)
|
||||||
|
rel_path = f"{base}.d/{stored_name}"
|
||||||
|
attachments_meta.append({
|
||||||
|
"filename": orig_name,
|
||||||
|
"path": rel_path,
|
||||||
|
"content_type": content_type,
|
||||||
|
"size": len(payload_bytes),
|
||||||
|
})
|
||||||
|
payload = {
|
||||||
|
"version": 1,
|
||||||
|
"type": "incoming",
|
||||||
|
"id": msg_id_short,
|
||||||
|
"message_id": mid or "",
|
||||||
|
"from": from_addr,
|
||||||
|
"to": to_addrs,
|
||||||
|
"subject": subj,
|
||||||
|
"date": date_h or "",
|
||||||
|
"body": body or "",
|
||||||
|
"references": refs,
|
||||||
|
"in_reply_to": in_reply_to,
|
||||||
|
"uid": uid_s,
|
||||||
|
"created_at": created_at,
|
||||||
|
"issue_number": None,
|
||||||
|
"status": "pending",
|
||||||
|
"attachments": attachments_meta,
|
||||||
|
}
|
||||||
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
written += 1
|
||||||
|
print(f"[tickets-fetch-inbox] Wrote {path.name}")
|
||||||
|
|
||||||
|
mail.logout()
|
||||||
|
print(f"[tickets-fetch-inbox] Done. Wrote {written} new message(s).")
|
||||||
|
if skipped_fetch or skipped_no_project or skipped_from or skipped_pending or skipped_response:
|
||||||
|
print(
|
||||||
|
f"[tickets-fetch-inbox] Skipped: fetch/parse={skipped_fetch}, no_project_for_to={skipped_no_project}, "
|
||||||
|
f"from_not_allowed={skipped_from}, pending_exists={skipped_pending}, response_exists={skipped_response}."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
17
gitea-issues/tickets-fetch-inbox.sh
Executable file
17
gitea-issues/tickets-fetch-inbox.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetch inbox messages filtered by tickets.authorized_emails (conf.json). No UNSEEN; no mark read.
|
||||||
|
# Writes new messages to projects/<id>/data/issues/ as JSON (<date>.<id>.<from>.pending).
|
||||||
|
# Usage: cd <racine_projet> && ./ia_dev/gitea-issues/tickets-fetch-inbox.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
# Use same project root and env as list-pending-spooler (lib.sh) so spool path is identical.
|
||||||
|
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
|
||||||
|
export PROJECT_ROOT="${PROJECT_ROOT:-$ROOT}"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/tickets-fetch-inbox.py" "$@"
|
||||||
88
gitea-issues/wiki-api-test.sh
Executable file
88
gitea-issues/wiki-api-test.sh
Executable file
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Test Gitea Wiki API for repo 4nk/lecoffre_ng.
|
||||||
|
# Requires GITEA_TOKEN or .secrets/gitea-issues/token (same as issues scripts).
|
||||||
|
# Usage: ./wiki-api-test.sh [--create]
|
||||||
|
# --create: create a test page then delete it (checks write access).
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
|
||||||
|
# Branch ref for wiki (default branch of wiki repo; use master when wiki is configured on master)
|
||||||
|
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
|
||||||
|
WIKI_PAGES="${REPO_PATH}/wiki/pages?ref=${GITEA_WIKI_REF}"
|
||||||
|
WIKI_PAGE="${REPO_PATH}/wiki/page"
|
||||||
|
WIKI_NEW="${REPO_PATH}/wiki/new"
|
||||||
|
|
||||||
|
do_create=false
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--create) do_create=true; shift ;;
|
||||||
|
*) log_err "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! load_gitea_token 2>/dev/null; then
|
||||||
|
log_err "No GITEA_TOKEN and no .secrets/gitea-issues/token. Set token to run wiki API tests."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
echo "=== 1. GET ${WIKI_PAGES} (list wiki pages) ==="
|
||||||
|
RESPONSE="$(gitea_api_get "${WIKI_PAGES}")"
|
||||||
|
if echo "$RESPONSE" | jq -e . &>/dev/null; then
|
||||||
|
if echo "$RESPONSE" | jq -e 'type == "array"' &>/dev/null; then
|
||||||
|
COUNT="$(echo "$RESPONSE" | jq 'length')"
|
||||||
|
log_info "List OK: ${COUNT} page(s)"
|
||||||
|
echo "$RESPONSE" | jq -r '.[] | " - \(.title)"' 2>/dev/null || echo "$RESPONSE" | jq .
|
||||||
|
else
|
||||||
|
log_info "Response: $(echo "$RESPONSE" | jq -c . 2>/dev/null || echo "$RESPONSE")"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_err "Response (first 300 chars): ${RESPONSE:0:300}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 2. GET ${WIKI_PAGE}/Home (get one page, ref=${GITEA_WIKI_REF}) ==="
|
||||||
|
RESPONSE="$(gitea_api_get "${WIKI_PAGE}/Home?ref=${GITEA_WIKI_REF}")"
|
||||||
|
if echo "$RESPONSE" | jq -e .title &>/dev/null; then
|
||||||
|
log_info "Page OK: title=$(echo "$RESPONSE" | jq -r .title)"
|
||||||
|
echo "$RESPONSE" | jq '{ title, html_url, commit_count }'
|
||||||
|
else
|
||||||
|
log_info "Response: $(echo "$RESPONSE" | jq -c . 2>/dev/null || echo "${RESPONSE:0:200}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$do_create" != true ]]; then
|
||||||
|
log_info "Done. Use --create to test POST wiki page and DELETE."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 3. POST ${WIKI_NEW} (create test page) ==="
|
||||||
|
TEST_TITLE="Api-test-$(date +%s)"
|
||||||
|
CONTENT="# Test\nCreated by wiki-api-test.sh. Safe to delete."
|
||||||
|
CONTENT_B64="$(echo -n "$CONTENT" | base64 -w 0)"
|
||||||
|
BODY="$(jq -n --arg title "$TEST_TITLE" --arg content "$CONTENT_B64" --arg msg "wiki-api-test.sh" \
|
||||||
|
'{ title: $title, content_base64: $content, message: $msg }')"
|
||||||
|
RESPONSE="$(gitea_api_post "${WIKI_NEW}" "$BODY")"
|
||||||
|
if echo "$RESPONSE" | jq -e .title &>/dev/null; then
|
||||||
|
log_info "Create OK: $(echo "$RESPONSE" | jq -r .title)"
|
||||||
|
CREATED_TITLE="$TEST_TITLE"
|
||||||
|
else
|
||||||
|
log_err "Create failed: ${RESPONSE:0:300}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 4. DELETE ${WIKI_PAGE}/${CREATED_TITLE} (remove test page) ==="
|
||||||
|
RESPONSE="$(gitea_api_delete "${WIKI_PAGE}/${CREATED_TITLE}")"
|
||||||
|
# DELETE often returns 204 No Content
|
||||||
|
log_info "Delete sent (204 or empty body = success)."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "All wiki API tests completed."
|
||||||
34
gitea-issues/wiki-get-page.sh
Executable file
34
gitea-issues/wiki-get-page.sh
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Output the raw markdown of a wiki page (for agents or scripts).
|
||||||
|
# Usage: ./wiki-get-page.sh <page_name>
|
||||||
|
# Example: ./wiki-get-page.sh Home
|
||||||
|
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
|
||||||
|
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
log_err "Usage: $0 <page_name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PAGE_NAME="$1"
|
||||||
|
load_gitea_token || exit 1
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${PAGE_NAME}?ref=${GITEA_WIKI_REF}")"
|
||||||
|
if ! echo "$resp" | jq -e .content_base64 &>/dev/null; then
|
||||||
|
log_err "Page not found or error: ${PAGE_NAME}"
|
||||||
|
echo "$resp" | jq . 2>/dev/null || echo "$resp"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$resp" | jq -r '.content_base64' | base64 -d
|
||||||
|
echo
|
||||||
100
gitea-issues/wiki-migrate-docs.sh
Executable file
100
gitea-issues/wiki-migrate-docs.sh
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Migrate all docs/*.md (repo root) to Gitea wiki as pages.
|
||||||
|
# Mapping: docs/FILE.md → page "File" (stem with _ → -, first letter upper per segment).
|
||||||
|
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
|
||||||
|
# Usage: ./wiki-migrate-docs.sh [--dry-run] [file.md ...]
|
||||||
|
# --dry-run: print mapping and skip API calls.
|
||||||
|
# If file(s) given: migrate only those; else migrate all docs/*.md.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
DOCS_DIR="${REPO_ROOT}/docs"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
|
||||||
|
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
|
||||||
|
WIKI_PAGE="${REPO_PATH}/wiki/page"
|
||||||
|
WIKI_NEW="${REPO_PATH}/wiki/new"
|
||||||
|
|
||||||
|
# docs/FILE.md → page name for wiki (stem: _ → -, title-case: First-Letter-Of-Each-Segment)
|
||||||
|
file_to_page_name() {
|
||||||
|
local base="$1"
|
||||||
|
local stem="${base%.md}"
|
||||||
|
echo "$stem" | tr '_' '-' | awk -F- '{
|
||||||
|
for(i=1;i<=NF;i++) {
|
||||||
|
s = $i; l = length(s)
|
||||||
|
if (l > 0) $i = toupper(substr(s,1,1)) tolower(substr(s,2))
|
||||||
|
}
|
||||||
|
}1' OFS='-'
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=false
|
||||||
|
files=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) dry_run=true; shift ;;
|
||||||
|
*.md) files+=("$1"); shift ;;
|
||||||
|
*) log_err "Unknown option or not .md: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
files+=("$f")
|
||||||
|
done < <(find "$DOCS_DIR" -maxdepth 1 -name '*.md' -print0 | sort -z)
|
||||||
|
else
|
||||||
|
# Resolve args to full paths under DOCS_DIR
|
||||||
|
for i in "${!files[@]}"; do
|
||||||
|
u="${files[$i]}"
|
||||||
|
if [[ "$u" != */* ]] && [[ -f "${DOCS_DIR}/${u}" ]]; then
|
||||||
|
files[$i]="${DOCS_DIR}/${u}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
log_err "No .md files found in ${DOCS_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" == true ]]; then
|
||||||
|
log_info "Dry run: would migrate ${#files[@]} file(s)"
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
base="$(basename "$f")"
|
||||||
|
page="$(file_to_page_name "$base")"
|
||||||
|
echo " $f → $page"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
load_gitea_token || exit 1
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
base="$(basename "$f")"
|
||||||
|
page="$(file_to_page_name "$base")"
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
log_err "Skip (not a file): $f"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
content="$(cat "$f")"
|
||||||
|
content_b64="$(echo -n "$content" | base64 -w 0)"
|
||||||
|
body="$(jq -n --arg title "$page" --arg content "$content_b64" --arg msg "Migrate from docs/$base" \
|
||||||
|
'{ title: $title, content_base64: $content, message: $msg }')"
|
||||||
|
# Check if page exists (GET); if 200 use PATCH else POST
|
||||||
|
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${page}?ref=${GITEA_WIKI_REF}")"
|
||||||
|
if echo "$resp" | jq -e .title &>/dev/null; then
|
||||||
|
log_info "Update: $base → $page"
|
||||||
|
patch_body="$(jq -n --arg content "$content_b64" --arg msg "Update from docs/$base" '{ content_base64: $content, message: $msg }')"
|
||||||
|
gitea_api_patch "${WIKI_PAGE}/${page}?ref=${GITEA_WIKI_REF}" "$patch_body" >/dev/null || true
|
||||||
|
else
|
||||||
|
log_info "Create: $base → $page"
|
||||||
|
gitea_api_post "${WIKI_NEW}" "$body" >/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Migration done: ${#files[@]} file(s)."
|
||||||
46
gitea-issues/wiki-put-page.sh
Executable file
46
gitea-issues/wiki-put-page.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Update a single wiki page from a local file.
|
||||||
|
# Usage: ./wiki-put-page.sh <page_name> <file_path>
|
||||||
|
# Example: ./wiki-put-page.sh Home docs/README.md
|
||||||
|
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "${GITEA_ISSUES_DIR}/lib.sh"
|
||||||
|
|
||||||
|
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
|
||||||
|
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
|
||||||
|
WIKI_PAGE="${REPO_PATH}/wiki/page"
|
||||||
|
WIKI_NEW="${REPO_PATH}/wiki/new"
|
||||||
|
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
log_err "Usage: $0 <page_name> <file_path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PAGE_NAME="$1"
|
||||||
|
FILE_PATH="$2"
|
||||||
|
[[ -f "$FILE_PATH" ]] || { log_err "File not found: $FILE_PATH"; exit 1; }
|
||||||
|
|
||||||
|
load_gitea_token || exit 1
|
||||||
|
require_jq || exit 1
|
||||||
|
|
||||||
|
content="$(cat "$FILE_PATH")"
|
||||||
|
content_b64="$(echo -n "$content" | base64 -w 0)"
|
||||||
|
msg="Update from $FILE_PATH"
|
||||||
|
|
||||||
|
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${PAGE_NAME}?ref=${GITEA_WIKI_REF}")"
|
||||||
|
if echo "$resp" | jq -e .title &>/dev/null; then
|
||||||
|
log_info "PATCH ${PAGE_NAME}"
|
||||||
|
body="$(jq -n --arg title "$PAGE_NAME" --arg content "$content_b64" --arg msg "$msg" '{ title: $title, content_base64: $content, message: $msg }')"
|
||||||
|
gitea_api_patch "${WIKI_PAGE}/${PAGE_NAME}?ref=${GITEA_WIKI_REF}" "$body"
|
||||||
|
else
|
||||||
|
log_info "POST ${PAGE_NAME}"
|
||||||
|
body="$(jq -n --arg title "$PAGE_NAME" --arg content "$content_b64" --arg msg "$msg" '{ title: $title, content_base64: $content, message: $msg }')"
|
||||||
|
gitea_api_post "${WIKI_NEW}" "$body"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Done: ${PAGE_NAME}"
|
||||||
66
gitea-issues/write-response-spooler.py
Normal file
66
gitea-issues/write-response-spooler.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Update the single spooler file for a message after sending a reply.
|
||||||
|
One file per message: read the .pending file (same base), add response data and set status to responded, write back.
|
||||||
|
No separate .response file; no file deletion.
|
||||||
|
Usage: ./gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]
|
||||||
|
base = filename base without extension (e.g. 2026-03-14T094530.a1b2c3d4.user_example.com).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from project_config import data_issues_dir, load_project_config
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
conf = load_project_config()
|
||||||
|
if not conf:
|
||||||
|
print("[write-response-spooler] No project config.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
spool = data_issues_dir()
|
||||||
|
spool.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser(description="Update spooler file in place after sending reply")
|
||||||
|
ap.add_argument("--base", required=True, help="Base name (e.g. 2026-03-14T094530.a1b2c3d4.user_example.com)")
|
||||||
|
ap.add_argument("--to", required=True, help="Recipient address")
|
||||||
|
ap.add_argument("--subject", required=True, help="Subject sent")
|
||||||
|
ap.add_argument("--body", required=True, help="Body sent")
|
||||||
|
ap.add_argument("--in-reply-to", default="", help="Message-ID we replied to")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
base = args.base.strip()
|
||||||
|
if not base or "/" in base or ".." in base:
|
||||||
|
print("[write-response-spooler] Invalid --base.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
path = spool / f"{base}.pending"
|
||||||
|
if not path.exists():
|
||||||
|
print(f"[write-response-spooler] No such file: {path.name}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
print(f"[write-response-spooler] Read error: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
data["status"] = "responded"
|
||||||
|
data["response"] = {
|
||||||
|
"in_reply_to_message_id": args.in_reply_to or "",
|
||||||
|
"to": args.to.strip(),
|
||||||
|
"subject": args.subject.strip(),
|
||||||
|
"body": args.body,
|
||||||
|
"sent_at": now,
|
||||||
|
}
|
||||||
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(f"[write-response-spooler] Updated {path.name} (status=responded)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
8
gitea-issues/write-response-spooler.sh
Executable file
8
gitea-issues/write-response-spooler.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Update the single spooler file (.pending) after sending a reply via mail-send-reply.sh. One file per message.
|
||||||
|
# Usage: ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]
|
||||||
|
set -euo pipefail
|
||||||
|
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
export GITEA_ISSUES_DIR
|
||||||
|
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
|
||||||
|
exec python3 "${GITEA_ISSUES_DIR}/write-response-spooler.py" "$@"
|
||||||
13
lib/README.md
Normal file
13
lib/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ia_dev shared lib
|
||||||
|
|
||||||
|
## project_config.sh
|
||||||
|
|
||||||
|
Sourced by deploy scripts and gitea-issues to resolve the current project **id** and the path to its JSON config.
|
||||||
|
|
||||||
|
**Before sourcing:** set `PROJECT_ROOT` (git repo root, where `ai_project_id` or `.ia_project` lives) and `IA_DEV_ROOT` (path to the `ia_dev` directory).
|
||||||
|
|
||||||
|
**After sourcing:** `PROJECT_SLUG` and `PROJECT_CONFIG_PATH` are set (and exported). Config path is `projects/<id>/conf.json`.
|
||||||
|
|
||||||
|
**Project id resolution order:** `IA_PROJECT` env → `.ia_project` at PROJECT_ROOT → `ai_project_id` at PROJECT_ROOT.
|
||||||
|
|
||||||
|
See `projects/README.md` for the config schema.
|
||||||
68
lib/project_config.sh
Normal file
68
lib/project_config.sh
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#
|
||||||
|
# Project config resolution for ia_dev scripts.
|
||||||
|
# Source this after setting PROJECT_ROOT and IA_DEV_ROOT.
|
||||||
|
# Resolves PROJECT_SLUG (id) and PROJECT_CONFIG_PATH (projects/<id>/conf.json).
|
||||||
|
#
|
||||||
|
# Project id resolution order:
|
||||||
|
# 1. MAIL_TO (env): search all projects for tickets.authorized_emails.to == MAIL_TO
|
||||||
|
# 2. AI_AGENT_TOKEN (env): search all projects/.secrets/<env>/ia_token for matching token; sets PROJECT_SLUG and PROJECT_ENV
|
||||||
|
# 3. IA_PROJECT (env)
|
||||||
|
# 4. .ia_project at PROJECT_ROOT (one line)
|
||||||
|
# 5. ai_project_id at PROJECT_ROOT (one line)
|
||||||
|
#
|
||||||
|
# Config file: projects/<id>/conf.json.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_SLUG=""
|
||||||
|
if [[ -n "${MAIL_TO:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
|
||||||
|
_to="$(echo "${MAIL_TO}" | sed 's/[[:space:]]//g' | tr '[:upper:]' '[:lower:]')"
|
||||||
|
for conf in "${IA_DEV_ROOT}/projects/"*/conf.json; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
_to_conf="$(jq -r '.tickets.authorized_emails.to // ""' "$conf" 2>/dev/null | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]//g')"
|
||||||
|
if [[ -n "$_to_conf" && "$_to_conf" = "$_to" ]]; then
|
||||||
|
PROJECT_SLUG="$(basename "$(dirname "$conf")")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
PROJECT_ENV=""
|
||||||
|
if [[ -z "$PROJECT_SLUG" && -n "${AI_AGENT_TOKEN:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
|
||||||
|
_token="$(echo "${AI_AGENT_TOKEN}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||||
|
for _pdir in "${IA_DEV_ROOT}/projects/"*/; do
|
||||||
|
[[ -d "${_pdir}.secrets" ]] || continue
|
||||||
|
_project="$(basename "$_pdir")"
|
||||||
|
for _envdir in "${_pdir}.secrets/"*/; do
|
||||||
|
[[ -f "${_envdir}ia_token" ]] || continue
|
||||||
|
_env="$(basename "$_envdir")"
|
||||||
|
_tok_conf="$(cat "${_envdir}ia_token" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||||
|
# Token is either full value in file or base + env (e.g. nicolecoffreio<env>)
|
||||||
|
if [[ -n "$_tok_conf" && ( "$_tok_conf" = "$_token" || "${_tok_conf}${_env}" = "$_token" ) ]]; then
|
||||||
|
PROJECT_SLUG="$_project"
|
||||||
|
PROJECT_ENV="$_env"
|
||||||
|
break 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
export PROJECT_ENV
|
||||||
|
if [[ -z "$PROJECT_SLUG" && -n "${IA_PROJECT:-}" ]]; then
|
||||||
|
PROJECT_SLUG="$(echo "${IA_PROJECT}" | sed 's/[[:space:]]//g')"
|
||||||
|
fi
|
||||||
|
if [[ -z "$PROJECT_SLUG" && -n "${PROJECT_ROOT:-}" && -f "$PROJECT_ROOT/.ia_project" ]]; then
|
||||||
|
PROJECT_SLUG="$(cat "$PROJECT_ROOT/.ia_project" | sed 's/[[:space:]]//g')"
|
||||||
|
fi
|
||||||
|
if [[ -z "$PROJECT_SLUG" && -n "${PROJECT_ROOT:-}" && -f "$PROJECT_ROOT/ai_project_id" ]]; then
|
||||||
|
PROJECT_SLUG="$(cat "$PROJECT_ROOT/ai_project_id" | sed 's/[[:space:]]//g')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_CONFIG_PATH=""
|
||||||
|
if [[ -n "$PROJECT_SLUG" && -n "${IA_DEV_ROOT:-}" ]]; then
|
||||||
|
PROJECT_CONFIG_PATH="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/conf.json"
|
||||||
|
if [[ ! -f "$PROJECT_CONFIG_PATH" ]]; then
|
||||||
|
PROJECT_CONFIG_PATH=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROJECT_SLUG
|
||||||
|
export PROJECT_CONFIG_PATH
|
||||||
53
projects/README.md
Normal file
53
projects/README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Project-specific configuration
|
||||||
|
|
||||||
|
This repo (`ia_dev`) is intended to be used as a **git submodule** inside each project. Project-specific parameters are stored in `projects/<id>/conf.json` (e.g. `projects/lecoffreio/conf.json`). The `<id>` is the project identifier and the name of the directory under `projects/`.
|
||||||
|
|
||||||
|
## Current project selection
|
||||||
|
|
||||||
|
There is no longer a "slug" in the URL or path; the project **id** (directory name under `projects/`) is resolved by:
|
||||||
|
|
||||||
|
**1. Mail ticketing**
|
||||||
|
From the **To** address of an email: search all `projects/*/conf.json` for `tickets.authorized_emails.to` equal to that address (case-insensitive). The matching project directory name is the id.
|
||||||
|
|
||||||
|
**2. API ai_working_help**
|
||||||
|
The token is found by scanning as described above: traverse all projects and all envs, reading each `projects/<id>/.secrets/<env>/ia_token` and comparing its content (or content + env) to the Bearer token. The first match yields the project id and the env (e.g. `test`, `pprod`, `prod`).
|
||||||
|
|
||||||
|
**3. Scripts / fallback** (when no request context)
|
||||||
|
- **`MAIL_TO`** (env): same as (1), resolve id by email "to".
|
||||||
|
- **`AI_AGENT_TOKEN`** (env): same as (2), resolve id (and env) by token; sets `PROJECT_SLUG` and `PROJECT_ENV`.
|
||||||
|
- **`IA_PROJECT`** (env)
|
||||||
|
- **`.ia_project`** at repository root (one line)
|
||||||
|
- **`ai_project_id`** at repository root (one line). When `ia_dev` is a submodule, this file lives at the host repo root (parent of `ia_dev`).
|
||||||
|
|
||||||
|
When running from a repo that has `ia_dev` as a submodule, the root is the parent repo; the script resolves `ia_dev` either as `./ia_dev` or `./deploy` (symlink to `ia_dev/deploy`).
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
One JSON file per project: `projects/<id>/conf.json` (e.g. `projects/lecoffreio/conf.json`). The `<id>` is the directory name; the config file is always named `conf.json`.
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `name` | yes | Human-readable project name |
|
||||||
|
| `project_path` | no | Relative path to project from ia_dev (e.g. `../lecoffre_ng_test`); used when running from ia_dev standalone |
|
||||||
|
| `build_dirs` | no | List of directories (relative to repo root) where `npm run build` is run before push. If missing or empty, build check is skipped |
|
||||||
|
| `version` | no | Version/bump configuration |
|
||||||
|
| `version.package_json_paths` | no | List of paths (relative to repo root) to `package.json` files to update on bump |
|
||||||
|
| `version.splash_app_name` | no | App name used in splash message template |
|
||||||
|
| `mail` | no | Mail/imap bridge config |
|
||||||
|
| `git` | no | Git hosting: `wiki_url`, `token_file` (path relative to repo root for token file). Ticketing URL is under `tickets`, not `git`. |
|
||||||
|
| `tickets` | no | Ticketing: `ticketing_url` (Gitea issues URL), `authorized_emails` (`to`: alias, `from`: list of allowed sender addresses for mail-based ticketing). The **to** address is used to resolve the project id when processing mails. See projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md. |
|
||||||
|
|
||||||
|
The API token for ai_working_help is **not** in conf.json. It is found by scanning all projects and all envs (as described above): each file `projects/<id>/.secrets/<env>/ia_token` is read and the Bearer is compared to the file content or to (file content + env). Token value is **base** + **env**; **env** is the environment name (test, pprod, prod), to be adapted per environment. The first match gives project id and env.
|
||||||
|
|
||||||
|
## Example (minimal)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My App",
|
||||||
|
"build_dirs": ["backend", "frontend"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example (full)
|
||||||
|
|
||||||
|
See `projects/lecoffreio/conf.json`.
|
||||||
37
projects/algo/conf.json
Normal file
37
projects/algo/conf.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"id": "algo",
|
||||||
|
"name": "algo",
|
||||||
|
"project_path": "/home/desk/code/algo/deploy",
|
||||||
|
"build_dirs": [
|
||||||
|
"/home/desk/code/algo/deploy/lecoffre-ressources-dev",
|
||||||
|
"/home/desk/code/algo/deploy/lecoffre-back-main",
|
||||||
|
"/home/desk/code/algo/deploy/lecoffre-front-main"
|
||||||
|
],
|
||||||
|
"deploy": {
|
||||||
|
"scripts_path": "/home/desk/code/algo/deploy/scripts_v2",
|
||||||
|
"deploy_script_path": "/home/desk/code/algo/deploy/scripts_v2/deploy.sh",
|
||||||
|
"secrets_path": "/home/desk/code/algo/.secrets"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"package_json_paths": [
|
||||||
|
"/home/desk/code/algo/deploy/lecoffre-back-main/package.json",
|
||||||
|
"/home/desk/code/algo/deploy/lecoffre-front-main/package.json"
|
||||||
|
],
|
||||||
|
"splash_app_name": "algo"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"email": "ai.support.algo@4nkweb.com",
|
||||||
|
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"wiki_url": "https://git.4nkweb.com/nicolas.cantu/algo/wiki",
|
||||||
|
"token_file": ".secrets/gitea-issues/token"
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/algo/issues",
|
||||||
|
"authorized_emails": {
|
||||||
|
"to": "ai.support.algo@4nkweb.com",
|
||||||
|
"from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
projects/enso/conf.json
Normal file
37
projects/enso/conf.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"id": "enso",
|
||||||
|
"name": "enso",
|
||||||
|
"project_path": "/home/desk/code/enso/deploy",
|
||||||
|
"build_dirs": [
|
||||||
|
"/home/desk/code/enso/deploy/lecoffre-ressources-dev",
|
||||||
|
"/home/desk/code/enso/deploy/lecoffre-back-main",
|
||||||
|
"/home/desk/code/enso/deploy/lecoffre-front-main"
|
||||||
|
],
|
||||||
|
"deploy": {
|
||||||
|
"scripts_path": "/home/desk/code/enso/deploy/scripts_v2",
|
||||||
|
"deploy_script_path": "/home/desk/code/enso/deploy/scripts_v2/deploy.sh",
|
||||||
|
"secrets_path": "/home/desk/code/enso/.secrets"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"package_json_paths": [
|
||||||
|
"/home/desk/code/enso/deploy/lecoffre-back-main/package.json",
|
||||||
|
"/home/desk/code/enso/deploy/lecoffre-front-main/package.json"
|
||||||
|
],
|
||||||
|
"splash_app_name": "enso"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"email": "ai.support.enso@4nkweb.com",
|
||||||
|
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"wiki_url": "https://git.4nkweb.com/nicolas.cantu/enso/wiki",
|
||||||
|
"token_file": ".secrets/gitea-issues/token"
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/enso/issues",
|
||||||
|
"authorized_emails": {
|
||||||
|
"to": "ai.support.enso@4nkweb.com",
|
||||||
|
"from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
projects/ia_dev/conf.json
Normal file
27
projects/ia_dev/conf.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"id": "ia_dev",
|
||||||
|
"name": "ia_dev",
|
||||||
|
"project_path": "../",
|
||||||
|
"base_path": "ia_dev",
|
||||||
|
"build_dirs": [],
|
||||||
|
"deploy": {},
|
||||||
|
"version": {
|
||||||
|
"package_json_paths": [],
|
||||||
|
"splash_app_name": "ia_dev"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"email": "ai.support.ia_dev@4nkweb.com",
|
||||||
|
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"wiki_url": "https://git.4nkweb.com/4nk/ia_dev/wiki",
|
||||||
|
"token_file": ".secrets/gitea-issues/token"
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"ticketing_url": "https://git.4nkweb.com/4nk/ia_dev/issues",
|
||||||
|
"authorized_emails": {
|
||||||
|
"to": "ai.support.ia_dev@4nkweb.com",
|
||||||
|
"from": ["nicolas.cantu@pm.me"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md
Normal file
159
projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Gitea issues – scripts et agents
|
||||||
|
|
||||||
|
Dossier dédié au traitement des tickets (issues) Gitea du dépôt **4nk/lecoffre_ng** (https://git.4nkweb.com/4nk/lecoffre_ng/issues). Toute la logique d'appel API et Git est dans les scripts ; les agents orchestrent et appellent /fix ou /evol.
|
||||||
|
|
||||||
|
## Contraintes de confidentialité et d'accès des agents
|
||||||
|
|
||||||
|
L'agent ne doit **jamais révéler** :
|
||||||
|
|
||||||
|
- Quelle IA il utilise
|
||||||
|
- Des informations personnelles
|
||||||
|
- Des informations sur 4NK
|
||||||
|
- Des informations sur l'équipe de réalisation
|
||||||
|
- Des informations sur des fichiers hors projet parent
|
||||||
|
- Des informations sur les autres questions et réponses traitées
|
||||||
|
|
||||||
|
L'agent ne doit **jamais accéder ni modifier** :
|
||||||
|
|
||||||
|
- Les fichiers hors projet (périmètre = dépôt parent et ia_dev dans ce dépôt)
|
||||||
|
- Les fichiers dans `.cursor` et `.secrets`
|
||||||
|
- D'autres branches que celle en cours de travail
|
||||||
|
- D'autres environnements (test / pprod / prod) que celui défini par le contexte
|
||||||
|
|
||||||
|
## Agents (.cursor/agents/)
|
||||||
|
|
||||||
|
| Agent | Fichier | Rôle |
|
||||||
|
|-------|---------|------|
|
||||||
|
| **agent-loop** | `agent-loop.md` | Orchestre la boucle de récupération des mails et le traitement par **exécutions délimitées** uniquement (N itérations ou x cycles) ; ne lance jamais de processus en arrière-plan (nohup/&). |
|
||||||
|
| **gitea-issues-process** | `gitea-issues-process.md` | Traite les issues Gitea et les mails en attente : liste les issues/mails, crée des branches, lance /fix ou /evol, /push-by-script ; workflow mails (fil, réponse réelle, marquage lu). |
|
||||||
|
|
||||||
|
Spooler tickets (nouveau) : mails dans **projects/<id>/data/issues/** (filtre par `tickets.authorized_emails` dans conf.json), pas de « non lu », aucun enregistrement supprimé. Format : `projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`. Récupération : `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`. Référence boucle mails (legacy) : `.cursor/agents/agent-loop.md`. Hook Cursor : `sessionStart` → `.cursor/hooks/remonter-mails.sh` (lit `projects/<id>/data/issues/*.pending`).
|
||||||
|
|
||||||
|
## Contexte d'exécution
|
||||||
|
|
||||||
|
- **Emplacement** : `gitea-issues/` est dans le sous-module **ia_dev** du dépôt projet. Chemin typique : `<racine_projet>/ia_dev/gitea-issues/`. Fonctionne pour tout projet utilisant ia_dev de la même manière.
|
||||||
|
- **Projet cible** : le projet est identifié **dynamiquement** par le fichier `../ai_project_id` (à la racine du dépôt projet, parent de `ia_dev`). Son contenu (slug) sert à charger la config dans `ia_dev/projects/<id>/`. Changer de projet = changer le dépôt (ou le contenu de `ai_project_id`) ; aucun chemin à hardcoder.
|
||||||
|
- **Lancement** : le chat Cursor et les scripts sont lancés depuis **ia_dev** (`<racine_projet>/ia_dev/`), mais les opérations (issues, mails, déploiement) concernent le dépôt **parent** (`../` = racine projet). Invoquer les scripts depuis la racine du dépôt projet : `cd <racine_projet> && ./ia_dev/gitea-issues/<script>.sh`. Depuis le workspace ia_dev, racine projet = `..`.
|
||||||
|
- **Secrets et logs** : `.secrets` est sous **ia_dev** (`ia_dev/.secrets`, soit `./.secrets` depuis ia_dev), et **ne dépend pas du projet parent**. Les scripts résolvent la racine pour `.secrets` et `logs` via le répertoire contenant `gitea-issues` (ia_dev), afin que la config IMAP/SMTP et les logs soient les mêmes quel que soit le projet ; un même clone ia_dev peut servir plusieurs projets configurés par `../ai_project_id`.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- **jq** : `apt install jq` ou `brew install jq`
|
||||||
|
- **Token Gitea** : variable d'environnement `GITEA_TOKEN` ou fichier `.secrets/gitea-issues/token` (contenu = le token, non versionné). Créer le token dans Gitea : Settings → Applications → Generate New Token (scopes `read:issue`, `write:issue` si commentaires).
|
||||||
|
|
||||||
|
## Scripts (depuis la racine du dépôt)
|
||||||
|
|
||||||
|
| Script | Usage | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `list-open-issues.sh` | `./gitea-issues/list-open-issues.sh [--lines] [--limit N]` | Liste les issues ouvertes (JSON ou lignes `number\|title\|state`). |
|
||||||
|
| `get-issue.sh` | `./gitea-issues/get-issue.sh <num> [--summary]` | Détail d'une issue (JSON ou résumé texte). |
|
||||||
|
| `print-issue-prompt.sh` | `./gitea-issues/print-issue-prompt.sh <num>` | Affiche titre + corps pour fournir la consigne à l'agent. |
|
||||||
|
| `create-branch-for-issue.sh` | `./gitea-issues/create-branch-for-issue.sh <num> [base]` | Crée et checkout la branche `issue/<num>` depuis `base` (défaut `test`). |
|
||||||
|
| `comment-issue.sh` | `./gitea-issues/comment-issue.sh <num> <message>` ou `echo "msg" \| ./gitea-issues/comment-issue.sh <num> -` | Ajoute un commentaire à l'issue. |
|
||||||
|
| `mail-list-unread.sh` | `./gitea-issues/mail-list-unread.sh` | Liste les mails **non lus envoyés à l'alias** (MAIL_FILTER_TO), **à partir du 10 mars 2026** (MAIL_SINCE_DATE) ; lecture seule ; sortie : UID, Message-ID, From, To, Subject, Date, Body. Aucun autre mail n'est listé. |
|
||||||
|
| `mail-get-thread.sh` | `./gitea-issues/mail-get-thread.sh <uid>` | Récupère **tout le fil** (conversation) du mail donné : tous les messages liés par References/In-Reply-To, tri chronologique (ancien → récent). Même format de sortie que mail-list-unread. À utiliser avant de décider ou répondre sur un mail. |
|
||||||
|
| `mail-send-reply.sh` | `./gitea-issues/mail-send-reply.sh --to <addr> --subject "..." [--body "..." \| stdin] [--in-reply-to "<msg-id>" [--references "..."]]` | Envoie une réponse par mail via le Bridge (SMTP) ; signature « Support IA du projet Lecoffre.io » / ai.support.lecoffreio@4nkweb.com ajoutée automatiquement. |
|
||||||
|
| `mail-create-issue-from-email.sh` | `./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..."]` | Crée une issue à partir d'un mail (UID), optionnel titre/corps formalisés ; marque le mail lu. |
|
||||||
|
| `mail-mark-read.sh` | `./gitea-issues/mail-mark-read.sh <uid>` | Marque un mail comme lu. |
|
||||||
|
| `mail-thread-log.sh` | `./gitea-issues/mail-thread-log.sh get-id \| init \| append-sent \| append-issue \| append-commit ...` | **Log par fil** : un fichier par conversation dans `projects/<id>/logs/gitea-issues/threads/` (échanges reçus/envoyés, tickets, commits). `get-id --uid <uid>` affiche `THREAD_ID=...` ; `init --uid <uid>` crée/met à jour le fichier ; `append-sent/issue/commit` enregistrent une réponse, une issue ou un commit. |
|
||||||
|
| `mail-to-issue.sh` | `./gitea-issues/mail-to-issue.sh` | **Batch** : crée une issue par mail non lu (titre = sujet, corps = texte + From), marque lus. À éviter si on suit le workflow agent (voir ci‑dessous). |
|
||||||
|
| `agent-loop.sh` | `./ia_dev/gitea-issues/agent-loop.sh [interval_sec]` | **Boucle de surveillance** : tourne indéfiniment (spooler ou legacy). **Ne pas lancer depuis l'agent** ; utiliser agent-loop-chat-iterations.sh ou cycles délimités. Voir `.cursor/agents/agent-loop.md`. |
|
||||||
|
| `agent-loop-treatment.sh` | `./ia_dev/gitea-issues/agent-loop-treatment.sh` | **Boucle traitement** : vérifie périodiquement `agent-loop.pending` ; si non vide, lance l'agent Cursor. Tourne indéfiniment ; **ne pas lancer depuis l'agent** (préférer agent-loop-chat-iterations.sh ou cycles délimités). |
|
||||||
|
| `agent-loop-retrieval-once.sh` | `./ia_dev/gitea-issues/agent-loop-retrieval-once.sh` | **Récupération une fois** (legacy, basé non lu) : exécute `mail-list-unread.sh` et écrit dans `agent-loop.pending`. Utilisé par l'agent agent-loop pour les cycles « x fois ». |
|
||||||
|
| `agent-loop-lock-acquire.sh` | `./ia_dev/gitea-issues/agent-loop-lock-acquire.sh` | **Lock (section 2)** : acquiert `agent-loop.lock` si absent ou périmé (>24 h). Exit 1 si lock actif ; l'agent ne doit pas lancer une deuxième instance. |
|
||||||
|
| `agent-loop-lock-release.sh` | `./ia_dev/gitea-issues/agent-loop-lock-release.sh` | **Lock (section 2)** : supprime `agent-loop.lock` et `agent-loop.stop`. À exécuter en fin de cycles (normale ou arrêt). |
|
||||||
|
| `agent-loop-stop.sh` | `./ia_dev/gitea-issues/agent-loop-stop.sh` | **Arrêt à la demande** : crée `agent-loop.stop` ; l'instance en cours s'arrête au début du cycle suivant. |
|
||||||
|
| `agent-loop-stop-requested.sh` | `./ia_dev/gitea-issues/agent-loop-stop-requested.sh` | **Vérification arrêt** : exit 0 si `agent-loop.stop` existe (utilisé par l'agent au début de chaque cycle). |
|
||||||
|
| `agent-loop-is-running.sh` | `./ia_dev/gitea-issues/agent-loop-is-running.sh` | **Vérifier si l'agent est en cours** : affiche fichier lock, PID et date ; indique si le processus est actif ou lock orphelin. Exit 0 si instance en cours (lock mtime < 24 h), 1 sinon. |
|
||||||
|
| `tickets-fetch-inbox.sh` | `./ia_dev/gitea-issues/tickets-fetch-inbox.sh` | **Récupération par expéditeurs autorisés** : mails **à partir du 10 mars 2026** (MAIL_SINCE_DATE), filtre `tickets.authorized_emails` (conf.json), pas de marquage lu/non lu. Écrit les nouveaux mails dans `projects/<id>/data/issues/*.pending` (JSON). Ne crée pas de .pending si un .response existe déjà. Voir `projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`. |
|
||||||
|
| `list-pending-spooler.sh` | `./ia_dev/gitea-issues/list-pending-spooler.sh` | **Spooler** : liste les fichiers `projects/<id>/data/issues/*.pending` qui n'ont **pas** de fichier `.response` correspondant (mails à traiter). Une ligne par chemin. |
|
||||||
|
| `write-response-spooler.sh` | `./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]` | **Spooler** : après envoi réussi avec `mail-send-reply.sh`, écrit `projects/<id>/data/issues/<base>.response` (JSON de la réponse envoyée). |
|
||||||
|
|
||||||
|
Variables optionnelles : `GITEA_API_URL`, `GITEA_REPO_OWNER`, `GITEA_REPO_NAME`, `GITEA_ISSUES_DIR`.
|
||||||
|
|
||||||
|
### Fichiers témoins et logs (boucle agent)
|
||||||
|
|
||||||
|
Sous `projects/<id>/logs/gitea-issues/` :
|
||||||
|
|
||||||
|
- **agent-loop.status** : fichier témoin de la boucle de surveillance (`agent-loop.sh` ou `agent-loop-retrieval-once.sh`). Trois lignes : horodatage (ISO), statut (`running` ou `idle`), détail (ex. « Aucun mail non lu »). Mis à jour à chaque itération. Si la date de modification est plus récente que 2× l’intervalle (ex. 120 s pour intervalle 60 s), la boucle est considérée active.
|
||||||
|
- **agent-loop.pending** : liste des mails non lus (legacy) écrite par `mail-list-unread.sh` via la boucle ; l’agent gitea-issues-process traite ce fichier.
|
||||||
|
- **agent-loop.lock** : lock pour une seule instance (section 2). Contenu : PID + date. Si mtime < 24 h, une nouvelle instance ne doit pas démarrer. Géré par `agent-loop-lock-acquire.sh` / `agent-loop-lock-release.sh`.
|
||||||
|
- **agent-loop.stop** : si présent, l'instance en cours s'arrête au début du cycle suivant. Création : `agent-loop-stop.sh` ou `touch …/agent-loop.stop`.
|
||||||
|
- **agent-loop-chat-iterations.log** : sortie de `agent-loop-chat-iterations.sh` (test d’envoi au lancement, puis à chaque itération le résultat de `mail-list-unread.sh`).
|
||||||
|
|
||||||
|
Le répertoire **projects/<id>/data/issues/** (spooler) est rempli **uniquement** par `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`. Si ce script n'est pas exécuté, `data/issues` reste vide si on n’utilise que la boucle legacy utilise seulement mail-list-unread.sh et agent-loop.pending dans logs/gitea-issues/.
|
||||||
|
|
||||||
|
**Réponse mail** : le `--body` de `mail-send-reply.sh` doit être **uniquement le texte rédigé par l’agent** (la réponse à l’expéditeur). Ne jamais y mettre le mail reçu, le sujet, une citation ou un bloc « From: » / « Message-ID » : sinon le destinataire reçoit son propre message au lieu de la réponse.
|
||||||
|
|
||||||
|
### Création d'issues depuis les mails (IMAP) – workflow agent
|
||||||
|
|
||||||
|
**Ne pas enchaîner directement** : l'agent doit d'abord lire les non lus, formaliser l'issue ou répondre par mail, et ne créer/traiter qu'au moment où la demande est prête.
|
||||||
|
|
||||||
|
1. **Lire les non lus** : `./gitea-issues/mail-list-unread.sh` (ne marque pas les mails comme lus).
|
||||||
|
2. **Pour chaque mail** : consulter **tout l'historique du fil** avec `./gitea-issues/mail-get-thread.sh <uid>`, créer/mettre à jour le **log du fil** avec `./gitea-issues/mail-thread-log.sh init --uid <uid>` (sortie `THREAD_ID=...` à conserver), puis décider soit d'envoyer une réponse directe (demande d'infos) via `mail-send-reply.sh`, soit de formaliser et créer l'issue avec `mail-create-issue-from-email.sh` (optionnel `--title` / `--body` formalisés). Si la demande est une correction/évolution prête : créer l'issue, traiter (fix/evol), commenter l'issue, répondre au mail via `mail-send-reply.sh` (avec `--in-reply-to` pour le fil). **Le corps de la réponse** doit contenir la **réponse réelle** à la question (ex. si le mail demande « Décrit les rôles », le body = une description des rôles), jamais le sujet du mail ni la question reçue.
|
||||||
|
3. **Réponses aux mails** : toujours via le Bridge avec `mail-send-reply.sh`. Le `--body` doit être la **réponse réelle** rédigée par l'agent (contenu de la réponse à la demande), pas le sujet du mail, pas la question reçue, pas un message précédent du fil. Chaque envoi est enregistré dans le log du fil avec `mail-thread-log.sh append-sent --thread-id <id> --to <addr> --subject "..." --body "..."` pour tracer l'expéditeur, le titre et le corps de la réponse envoyée.
|
||||||
|
|
||||||
|
**Prérequis :**
|
||||||
|
|
||||||
|
- Python 3 (stdlib : imaplib, email, smtplib, json, urllib).
|
||||||
|
- Token Gitea : comme les autres scripts (`GITEA_TOKEN` ou `.secrets/gitea-issues/token`).
|
||||||
|
- Config IMAP/SMTP : copier `gitea-issues/imap-bridge.env.example` vers `.secrets/gitea-issues/imap-bridge.env`. Pour la boucle agent (optionnel) : `agent-loop.env.example` vers `.secrets/gitea-issues/agent-loop.env` (voir `.cursor/agents/agent-loop.md`). Renseigner `IMAP_USER`, `IMAP_PASSWORD` (et optionnellement `SMTP_*` pour l'envoi ; par défaut SMTP reprend les mêmes host/port Bridge 1025). Optionnel : `MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com` (seuls les mails envoyés à cette adresse sont listés) ; `MAIL_SINCE_DATE=10-Mar-2026` (seuls les mails à partir de cette date sont récupérés/listés).
|
||||||
|
- Proton Mail Bridge (ou serveur IMAP/SMTP) en cours d'exécution.
|
||||||
|
|
||||||
|
**Scripts :** `mail-list-unread.sh`, `mail-get-thread.sh`, `mail-thread-log.sh`, `mail-send-reply.sh`, `mail-create-issue-from-email.sh`, `mail-mark-read.sh`. Le script batch `mail-to-issue.sh` reste disponible mais ne doit pas être utilisé dans le cadre du workflow agent (liste → lecture du fil → log du fil → décision → création/ réponse). Le script **`agent-loop.sh`** permet de lancer une boucle de surveillance des mails avec fichier témoin ; voir `.cursor/agents/agent-loop.md`.
|
||||||
|
|
||||||
|
## API Wiki (tests préalables)
|
||||||
|
|
||||||
|
Script de test de l'API Wiki Gitea pour le même dépôt (prérequis à une éventuelle migration de `docs/` vers le wiki) :
|
||||||
|
|
||||||
|
| Script | Usage | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `wiki-api-test.sh` | `./gitea-issues/wiki-api-test.sh [--create]` | Teste GET list pages, GET page Home ; avec `--create` : POST une page test puis DELETE. |
|
||||||
|
|
||||||
|
**Prérequis :** même token que les issues (`GITEA_TOKEN` ou `.secrets/gitea-issues/token`). Pour l'écriture (création / suppression de pages), le token doit avoir les droits d'écriture sur le dépôt.
|
||||||
|
|
||||||
|
**Endpoints utilisés (référence Gitea API 1.25) :**
|
||||||
|
|
||||||
|
- `GET /repos/{owner}/{repo}/wiki/pages` — liste des pages
|
||||||
|
- `GET /repos/{owner}/{repo}/wiki/page/{pageName}` — contenu d'une page (ex. `Home`)
|
||||||
|
- `POST /repos/{owner}/{repo}/wiki/new` — créer une page (body : `title`, `content_base64`, `message`)
|
||||||
|
- `PATCH /repos/{owner}/{repo}/wiki/page/{pageName}` — modifier une page
|
||||||
|
- `DELETE /repos/{owner}/{repo}/wiki/page/{pageName}` — supprimer une page
|
||||||
|
|
||||||
|
Si le wiki n'a jamais été initialisé (aucune page créée via l'interface), les GET peuvent renvoyer 404 ou une liste vide. **Initialiser le wiki** : aller sur https://git.4nkweb.com/4nk/lecoffre_ng/wiki et créer au moins une page (ex. « Home ») via l'interface, puis relancer le script avec un token valide.
|
||||||
|
|
||||||
|
**Branche par défaut du wiki :** si l'API renvoie `object does not exist [id: refs/heads/master]` alors que la branche par défaut du dépôt wiki est autre (ex. `prod`), c'est un bug connu de certaines versions de Gitea (l'API suppose `master`). Contournements possibles : (1) **mettre à jour Gitea** (correctif dans les versions récentes, ex. PR #34244) ; (2) **changer la branche par défaut du wiki** en `master` dans les réglages du dépôt (Settings → Branches). Variable optionnelle `GITEA_WIKI_REF=master` (défaut si wiki configuré sur master).
|
||||||
|
|
||||||
|
### Migration docs/ → wiki
|
||||||
|
|
||||||
|
**Décision :** tout le contenu de `docs/` (racine du dépôt) est migré vers le wiki ; pas de CI sur le wiki.
|
||||||
|
|
||||||
|
**Script de migration :**
|
||||||
|
|
||||||
|
| Script | Usage | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `wiki-migrate-docs.sh` | `./gitea-issues/wiki-migrate-docs.sh [--dry-run] [fichier.md ...]` | Migre `docs/*.md` vers le wiki. `--dry-run` affiche le mapping sans appel API. Si des fichiers sont passés en argument, migre uniquement ceux-là. |
|
||||||
|
| `wiki-put-page.sh` | `./gitea-issues/wiki-put-page.sh <page_name> <file_path>` | Met à jour ou crée une page wiki à partir d'un fichier local (ex. `Home docs/README.md`). |
|
||||||
|
| `wiki-get-page.sh` | `./gitea-issues/wiki-get-page.sh <page_name>` | Affiche le markdown brut d'une page wiki (pour scripts ou agents). |
|
||||||
|
|
||||||
|
**Correspondance fichier → page wiki :** nom de fichier sans `.md`, `_` remplacé par `-`, title-case par segment. Ex. OPERATIONS.md → Operations, README.md → Readme.
|
||||||
|
|
||||||
|
Les 17 fichiers de `docs/` ont été migrés ; les pages sont visibles sur https://git.4nkweb.com/4nk/lecoffre_ng/wiki. La page **Home** contient le contenu de `docs/README.md` (index et correspondance). **`docs/` est exclu du versionnement** (`.gitignore`) : maintenir `docs/` localement (ne pas le supprimer), pousser les modifications vers le wiki avec `wiki-migrate-docs.sh` ou `wiki-put-page.sh` ; ne pas committer `docs/`.
|
||||||
|
|
||||||
|
### Après un clone
|
||||||
|
|
||||||
|
Le répertoire `docs/` n'est pas versionné. Pour disposer d'une copie locale (édition puis synchro wiki), recréer le contenu à partir du wiki : ex. `./gitea-issues/wiki-get-page.sh Home > docs/README.md`, ou créer les fichiers manuellement à partir des pages wiki listées dans la section Migration ci-dessus.
|
||||||
|
|
||||||
|
### Usage « wiki uniquement » pour les agents
|
||||||
|
|
||||||
|
La connaissance du projet peut reposer **uniquement sur le wiki** (sans lire `docs/`) : les agents peuvent exécuter `./gitea-issues/wiki-get-page.sh <PageName>` pour récupérer le contenu markdown d'une page et l'utiliser comme référence. Exemples : `./gitea-issues/wiki-get-page.sh Home`, `./gitea-issues/wiki-get-page.sh Operations`, `./gitea-issues/wiki-get-page.sh Code-Standards`. Prérequis : token Gitea (comme pour les autres scripts wiki). Les agents peuvent ainsi consulter la doc projet à la demande depuis le wiki, sans dépendre des fichiers locaux `docs/`. La documentation des projets gérés est dans **`projects/<id>/docs`** (ex. `projects/lecoffreio/docs`) ; la documentation propre à ia_dev est dans **`projects/ia_dev/docs`**.
|
||||||
|
|
||||||
|
## Agents (commandes)
|
||||||
|
|
||||||
|
- **/agent-loop** (`agent-loop.md`) : gère les boucles par exécutions délimitées uniquement (N itérations avec `agent-loop-chat-iterations.sh [N]`, ou x cycles récupération + traitement). Ne lance jamais agent-loop.sh ni agent-loop-treatment.sh en arrière-plan.
|
||||||
|
- **/gitea-issues-process** (`gitea-issues-process.md`) : traite les issues Gitea et les mails en attente (workflow script au maximum, /fix ou /evol, /push-by-script). Voir le fichier de l'agent pour le workflow exact.
|
||||||
|
|
||||||
|
## Référence
|
||||||
|
|
||||||
|
- Wiki : https://git.4nkweb.com/4nk/lecoffre_ng/wiki
|
||||||
|
- Documentation opérationnelle (ex. `docs/OPERATIONS.md`) : page wiki **Operations** (après migration).
|
||||||
13
projects/ia_dev/docs/README.md
Normal file
13
projects/ia_dev/docs/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Documentation générique (ia_dev)
|
||||||
|
|
||||||
|
Ce répertoire contient les documents **non spécifiques à un projet** (ex. LeCoffre.io), réutilisables pour tout projet piloté par ia_dev.
|
||||||
|
|
||||||
|
**Emplacements de la documentation :**
|
||||||
|
- **Projets gérés** : `projects/<id>/docs` (ex. `projects/lecoffreio/docs`).
|
||||||
|
- **ia_dev (ce dépôt)** : `projects/ia_dev/docs` (ce répertoire).
|
||||||
|
|
||||||
|
- **agents-scripts-split.md** : Répartition des rôles entre agents Cursor et scripts (branch-align, change-to-all-branches, deploy, push), exécution depuis la racine, options standardisées.
|
||||||
|
- **WORKFLOWS_AND_COMPONENTS.md** : Workflows IA (clôture, déploiement, fix-lint, pousse, docupdate, audit, branch-align), plans Cursor, règles, subagents, commandes, skills, MCP, protocole de développement (générique, sans référence à un projet particulier).
|
||||||
|
- **GITEA_ISSUES_SCRIPTS_AGENTS.md** : Gitea issues – scripts et agents (liste des scripts, workflow mails, boucle agent, prérequis).
|
||||||
|
- **TICKETS_SPOOL_FORMAT.md** : Format JSON du spooler tickets (projects/<id>/data/issues/), schémas incoming/response, pièces jointes, config conf.json.
|
||||||
|
- **ai_working_help/docs/notary-ai-api.md** : API IA notaire (enqueue, response), spooler projects/<id>/data/notary-ai/, scripts notary-ai/, agents notary-ai-loop et notary-ai-process.
|
||||||
135
projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md
Normal file
135
projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# Format JSON du spooler tickets (projects/<id>/data/issues/)
|
||||||
|
|
||||||
|
Les mails et réponses sont stockés dans **projects/<id>/data/issues/** (sous ia_dev, par projet). **Il n'y a toujours qu'un seul fichier de spooler par message reçu** — pas un fichier par état : le même fichier change d'extension et de contenu quand on passe de « à traiter » à « répondu ». Seuls les messages **à partir du 10 mars 2026** (ou la date `MAIL_SINCE_DATE` en env) sont récupérés. Filtrage par **expéditeurs autorisés** (conf.json → `tickets.authorized_emails`), sans s'appuyer sur le statut « non lu ». Le traitement se base sur l'extension du fichier (`.pending` = à traiter ; après réponse, le fichier est renommé en `.response` et son contenu mis à jour avec la question reçue et la réponse envoyée).
|
||||||
|
|
||||||
|
## Emplacement et nommage des fichiers
|
||||||
|
|
||||||
|
**Un seul fichier de spooler par message reçu** : il n'y a pas un fichier par état. Un même message est représenté par un seul fichier ; quand le statut change (pending → response), on renomme ce fichier (changement d'extension) et on met à jour son contenu ; on ne crée pas de second fichier.
|
||||||
|
|
||||||
|
- **Racine** : `ia_dev/projects/<id>/data/issues/` (id = slug projet, ex. contenu de `ai_project_id` ou `.ia_project` à la racine du dépôt)
|
||||||
|
- **Format des noms** : `<date>.<id>.<from>.<status>` (un seul fichier par message ; le statut change en renommant l’extension et en mettant à jour le contenu).
|
||||||
|
- `date` : `YYYY-MM-DDTHHmmss` (ex. `2026-03-14T094530`)
|
||||||
|
- `id` : identifiant unique généré à la réception (déterministe à partir du message, ex. 8 caractères hexadécimaux). Même message → même `id`.
|
||||||
|
- `from` : adresse expéditeur rendue sûre pour le système de fichiers (ex. `user_example.com` pour `user@example.com`)
|
||||||
|
- `status` : extension du fichier : `pending` (à traiter) ou `response` (réponse envoyée ; le fichier `.pending` est alors supprimé)
|
||||||
|
- **Messages entrants** : `<date>.<id>.<from>.pending`. Créés par `tickets-fetch-inbox.sh`. À traiter = fichier `.pending` (il n'existe alors pas encore de `.response` pour ce message).
|
||||||
|
- **Changement de statut** : on ne modifie pas `<date>.<id>.<from>`. Après envoi réussi, le script `write-response-spooler.sh` renomme le fichier en `.response` et écrit le nouveau contenu (question reçue + réponse envoyée) ; l'ancien `.pending` est supprimé. Il reste donc un seul fichier par message.
|
||||||
|
- **Après réponse** : le fichier s'appelle `<date>.<id>.<from>.response` et contient la question reçue et la réponse envoyée (voir schéma response ci-dessous).
|
||||||
|
- **Pièces jointes (pj)** : pour un message de base `<base>` (ex. `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io`), les pièces jointes sont dans **`<base>.d/`**. Nommage des fichiers : `<index>_<nom_fichier_sanitifé>`. Le JSON du message contient le tableau `attachments` avec pour chaque entrée le chemin relatif à `data/issues/`, le type MIME et la taille.
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
- `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io.pending` — message entrant en attente
|
||||||
|
- `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io.response` — réponse envoyée (question reçue + réponse dans le JSON) ; le `.pending` associé a été supprimé
|
||||||
|
|
||||||
|
## Schéma JSON — message entrant (incoming)
|
||||||
|
|
||||||
|
Fichiers `.pending`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"type": "incoming",
|
||||||
|
"id": "a1b2c3d4",
|
||||||
|
"message_id": "<original.Message-ID@host>",
|
||||||
|
"from": "laurence@lecoffre.io",
|
||||||
|
"to": ["ai.support.lecoffreio@4nkweb.com"],
|
||||||
|
"subject": "Demande d'information",
|
||||||
|
"date": "Fri, 14 Mar 2026 09:45:30 +0100",
|
||||||
|
"body": "Texte brut du corps du mail.",
|
||||||
|
"references": ["<id1@host>", "<id2@host>"],
|
||||||
|
"in_reply_to": "<id2@host>",
|
||||||
|
"uid": "42",
|
||||||
|
"created_at": "2026-03-14T09:45:35Z",
|
||||||
|
"issue_number": null,
|
||||||
|
"status": "pending",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"filename": "document.pdf",
|
||||||
|
"path": "2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io.d/0_document.pdf",
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"size": 12345
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Champ | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `version` | number | Version du schéma (1) |
|
||||||
|
| `type` | string | `"incoming"` |
|
||||||
|
| `id` | string | Identifiant court du message (même que dans le nom de fichier) |
|
||||||
|
| `message_id` | string | En-tête Message-ID du mail |
|
||||||
|
| `from` | string | Adresse de l'expéditeur |
|
||||||
|
| `to` | string[] | Adresses destinataires |
|
||||||
|
| `subject` | string | Sujet |
|
||||||
|
| `date` | string | Date d'envoi (RFC2822 ou ISO8601) |
|
||||||
|
| `body` | string | Corps du message (texte brut) |
|
||||||
|
| `references` | string[] | En-tête References (liste de Message-IDs) |
|
||||||
|
| `in_reply_to` | string \| null | En-tête In-Reply-To |
|
||||||
|
| `uid` | string | UID IMAP (pour référence, pas pour marquer lu) |
|
||||||
|
| `created_at` | string | Date d'écriture dans le spool (ISO8601) |
|
||||||
|
| `issue_number` | number \| null | Numéro d'issue Gitea si une issue a été créée |
|
||||||
|
| `status` | string | `pending` |
|
||||||
|
| `attachments` | array | Pièces jointes : tableau d'objets (voir ci-dessous). `path` est relatif à `data/issues/` ; les fichiers sont dans `<base>.d/`. L'agent qui traite les tickets peut les lire depuis `projects/<id>/data/issues/<path>`. |
|
||||||
|
|
||||||
|
**Objet pièce jointe** : `filename` (nom d'origine), `path` (chemin relatif sous `projects/<id>/data/issues/`), `content_type` (MIME), `size` (octets).
|
||||||
|
|
||||||
|
## Schéma JSON — réponse envoyée (response)
|
||||||
|
|
||||||
|
Fichiers `.response`. Contiennent la **question reçue** et la **réponse envoyée**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"type": "response",
|
||||||
|
"received_question": {
|
||||||
|
"subject": "Demande d'information",
|
||||||
|
"body": "Texte brut du corps du mail reçu."
|
||||||
|
},
|
||||||
|
"sent_response": {
|
||||||
|
"in_reply_to_message_id": "<original.Message-ID@host>",
|
||||||
|
"to": "laurence@lecoffre.io",
|
||||||
|
"subject": "Re: Demande d'information",
|
||||||
|
"body": "Texte de la réponse envoyée par l'agent.",
|
||||||
|
"sent_at": "2026-03-14T10:12:00Z"
|
||||||
|
},
|
||||||
|
"created_at": "2026-03-14T10:12:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Champ | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `version` | number | Version du schéma (1) |
|
||||||
|
| `type` | string | `"response"` |
|
||||||
|
| `received_question` | object | Question reçue : `subject`, `body` |
|
||||||
|
| `sent_response` | object | Réponse envoyée : `in_reply_to_message_id`, `to`, `subject`, `body`, `sent_at` |
|
||||||
|
| `created_at` | string | Date de création du fichier (ISO8601) |
|
||||||
|
|
||||||
|
## Configuration projet (conf.json)
|
||||||
|
|
||||||
|
- **ticketing_url** et la config ticketing sont sous la clé **`tickets`** (et non plus sous `git`).
|
||||||
|
- **`tickets`** contient :
|
||||||
|
- `ticketing_url` : URL des issues Gitea
|
||||||
|
- `authorized_emails` :
|
||||||
|
- **`to`** : récupérer **uniquement** les mails **envoyés à** cette adresse (destinataire).
|
||||||
|
- **`from`** : liste des expéditeurs autorisés — **un élément par adresse** (tableau de chaînes). Ne pas mettre plusieurs adresses dans une seule chaîne.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"tickets": {
|
||||||
|
"ticketing_url": "https://git.4nkweb.com/4nk/lecoffre_ng/issues",
|
||||||
|
"authorized_emails": {
|
||||||
|
"to": "ai.support.lecoffreio@4nkweb.com",
|
||||||
|
"from": [
|
||||||
|
"ai.support.lecoffreio@4nkweb.com",
|
||||||
|
"laurence@lecoffre.io",
|
||||||
|
"nicolas.cantu@pm.me"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La récupération ne prend que les messages **envoyés à** `authorized_emails.to` **et** **expédités par** une des adresses de `authorized_emails.from`. Aucun marquage « lu / non lu ». Lorsqu'une réponse est enregistrée, le fichier `.pending` est supprimé et remplacé par le fichier `.response` (même base `<date>.<id>.<from>`).
|
||||||
|
|
||||||
|
**IMAP** : le script se connecte à la boîte IMAP configurée (ex. `.secrets/gitea-issues/imap-bridge.env`). Les messages sont filtrés selon les en-têtes **To**, **Delivered-To**, **X-Original-To**, etc. Pour que des messages « envoyés à » l'alias soient trouvés, **la boîte IMAP doit être celle qui reçoit le courrier pour cette adresse** (celle de `authorized_emails.to`). Si l'IMAP pointe vers une autre boîte (ex. une adresse personnelle qui reçoit tout), aucun message ne contiendra l'alias dans To/Delivered-To et tout sera exclu par `not_to_alias`. Il faut alors utiliser les identifiants IMAP de la boîte qui reçoit les mails envoyés à l'alias.
|
||||||
419
projects/ia_dev/docs/WORKFLOWS_AND_COMPONENTS.md
Normal file
419
projects/ia_dev/docs/WORKFLOWS_AND_COMPONENTS.md
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
# Workflows et composants IA
|
||||||
|
|
||||||
|
**Version** : 1.0.0
|
||||||
|
**Périmètre** : Règles Cursor, subagents, commandes, skills, workflows de développement (générique ia_dev)
|
||||||
|
|
||||||
|
Ce document formalise chaque workflow et chaque composant utilisés par l'IA pour le développement d'un projet piloté par ia_dev.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table des matières
|
||||||
|
|
||||||
|
1. [Vue d'ensemble](#1-vue-densemble)
|
||||||
|
2. [Workflows](#2-workflows)
|
||||||
|
3. [Plans Cursor (déclenchables)](#3-plans-cursor-déclenchables)
|
||||||
|
4. [Composants : Règles](#4-composants--règles)
|
||||||
|
5. [Composants : Subagents](#5-composants--subagents)
|
||||||
|
6. [Composants : Commandes](#6-composants--commandes)
|
||||||
|
7. [Composants : Skills](#7-composants--skills)
|
||||||
|
8. [MCP et outils](#8-mcp-et-outils)
|
||||||
|
9. [Usage](#9-usage)
|
||||||
|
10. [Protocole de développement](#10-protocole-de-développement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vue d'ensemble
|
||||||
|
|
||||||
|
| Type | Emplacement | Portée |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| Règles | `.cursor/rules/*.mdc` | Projet |
|
||||||
|
| Plans | `~/.cursor/plans/*.plan.md` | Utilisateur (utilisables depuis ce projet) |
|
||||||
|
| Commandes projet | `.cursor/commands/*.md` | Projet (priorité sur global, invoquent plans utilisateur) |
|
||||||
|
| Commandes globales | `~/.cursor/commands/*.md` | Utilisateur (tous projets) |
|
||||||
|
| Subagents | `~/.cursor/agents/*.md` | Utilisateur (tous projets) |
|
||||||
|
| Skills | `~/.cursor/skills-cursor/*/SKILL.md` | Utilisateur (tous projets) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Workflows
|
||||||
|
|
||||||
|
### 2.1 Workflow de clôture évolution / correction
|
||||||
|
|
||||||
|
**Déclencheur** : Fin de toute évolution ou correction.
|
||||||
|
**Règle** : `.cursor/rules/cloture-evolution.mdc` (alwaysApply: true).
|
||||||
|
|
||||||
|
**Ordre d'exécution** : Chaque bloc est bouclé jusqu'à stabilisation (rien à optimiser). **Réponse non partielle** : répondre à toutes les questions de clôture et lancer les subagents/plans concernés.
|
||||||
|
|
||||||
|
**Checklist de clôture (obligatoire)** : (1) Modifications similaires ailleurs ? (2) Optimisations/mutualisations possibles ? (3) Fichiers exempts d'erreurs lint ? (4) Types OK ? (5) Projet compile ?
|
||||||
|
|
||||||
|
**Orchestration** : Après correction, lancer en boucle : fix-lint (generalPurpose) si erreurs ; deploy (mcp_task) si validé ; en cas d'échec subagent, prendre le relais et corriger sans s'arrêter.
|
||||||
|
|
||||||
|
| Étape | Actions |
|
||||||
|
|-------|---------|
|
||||||
|
| 1. Tests | Compléter tests manquants (user stories) ; lancer ; corriger échecs ; rationaliser (supprimer doublons, mutualiser) |
|
||||||
|
| 2. Types | Chercher optimisations types (`npm run typecheck`, `npx tsc --noEmit`) ; implémenter si pertinent |
|
||||||
|
| 3. Reproductions | Chercher changements similaires à reproduire ailleurs ; implémenter si pertinent |
|
||||||
|
| 4. Factorisation | Chercher factorisation, mutualisation, centralisation ; implémenter si pertinent |
|
||||||
|
| 5. Lint | Chercher erreurs lint (`npm run lint`, `ReadLints`) ; corriger ; sinon vérifier si plus exigeant possible |
|
||||||
|
| 6. Sécurité | Chercher améliorations (`docs/CODE_SECURITY.md`) ; implémenter si pertinent |
|
||||||
|
| 7. Documentation | Mettre à jour REX + descriptions (README consolidation, FRONTEND.md, CODE_STANDARDS.md) ; rationaliser |
|
||||||
|
| 8. Déploiement | Après validation utilisateur : déployer ; analyser logs ; corrections/optimisations/sécurité déploiement ; doc déploiement |
|
||||||
|
|
||||||
|
**Contraintes** : Déploiement uniquement après validation. Pas de tâches en arrière-plan.
|
||||||
|
|
||||||
|
**Plan** : `.cursor/plans/workflow-cloture-evolution.plan.md`
|
||||||
|
**Commande** : `/cloture-evolution`
|
||||||
|
**Référence** : `docs/README.md#consolidation-operationnelle-ex-operationsmd`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Workflow de déploiement
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/deploy` ou invocation subagent deploy.
|
||||||
|
**Script** : `deploy/scripts_v2/deploy.sh` — s'exécute **localement** et orchestre le déploiement **à distance** (SSH vers la cible, git pull, build, restart services).
|
||||||
|
|
||||||
|
| Étape | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| 0 | Hook pre-deploy : arrêter (SIGTERM) les processus concurrents (lint, fix-lint, typecheck, turbopack) dont le cwd est dans le projet. Si des processus restent (hors projet), bloquer. Après déploiement : message pour relancer /fix-lint si nécessaire. |
|
||||||
|
| 1 | Vérifier que les modifications sont commitées |
|
||||||
|
| 2 | Exécuter `./deploy/scripts_v2/deploy.sh <env>` depuis la racine du projet, avec `test`, `pprod` ou `prod` |
|
||||||
|
| 3 | **En cas d'échec** : orchestrer la correction automatique (boucle jusqu'à succès ou impossibilité) : (a) analyser la cause (logs, journalctl, fichiers sur cible, dist) ; (b) corriger la root cause sans contournement ; (c) committer/pousser si besoin ; (d) relancer le script ; (e) répéter |
|
||||||
|
|
||||||
|
**Options** : `--skipSetupHost`, `--checkLint`. **Env** : `DEPLOY_SKIP_CONCURRENCY_CHECK=1` pour désactiver le hook pre-deploy.
|
||||||
|
**Plan** : `.cursor/plans/workflow-deploy.plan.md`
|
||||||
|
**Commande** : `/deploy <env>`
|
||||||
|
**Référence** : docs/DEPLOYMENT.md, deploy/README.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Workflow de correction lint
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/fix-lint` ou invocation subagent fix-lint.
|
||||||
|
**Périmètre** : applications du projet (front, back, ressources selon `projects/<id>/conf.json` build_dirs).
|
||||||
|
|
||||||
|
| Étape | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| 1 | Exécuter `npm run lint` dans chaque application pour lister les erreurs |
|
||||||
|
| 2 | Corriger par lots de 10 erreurs maximum |
|
||||||
|
| 3 | Entre chaque lot : lancer test build/typecheck |
|
||||||
|
| 4 | Ne jamais contourner (pas de `eslint-disable`) |
|
||||||
|
| 5 | Appliquer patterns : extraction helpers, découpage fichiers, objets de configuration |
|
||||||
|
|
||||||
|
**Règles applicables** : max-lines 250, max-lines-per-function 40, max-params 4, max-depth 4, complexity 10, max-nested-callbacks 3.
|
||||||
|
|
||||||
|
**Contournement mcp_task** : `subagent_type="fix-lint"` via mcp_task échoue ; utiliser `generalPurpose` avec prompt détaillé. Voir la consolidation dans `docs/README.md`.
|
||||||
|
**Plan** : `.cursor/plans/workflow-fix-lint.plan.md`
|
||||||
|
**Commande** : `/fix-lint`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Workflow de commit et push (pousse)
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/pousse` ou demande de commit/push.
|
||||||
|
|
||||||
|
| Étape | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| 1 | `git add -A` |
|
||||||
|
| 2 | `git commit -m "..."` avec format structuré (Motivations, Root causes, Correctifs, Evolutions, Pages affectées) |
|
||||||
|
| 3 | `git push` (sans déclencher CI si règle projet) |
|
||||||
|
|
||||||
|
**Format commit** : Titre court ; sections en bullets ; pas de `--no-verify` sauf cas documenté.
|
||||||
|
**Plan** : `.cursor/plans/workflow-pousse.plan.md`
|
||||||
|
**Commande** : `/pousse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 Workflow de mise à jour documentation (docupdate)
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/docupdate`.
|
||||||
|
|
||||||
|
**docs/features extract** : Extraire données de docs/features vers docs/ ; supprimer docs/features.
|
||||||
|
|
||||||
|
**docs/fixKnowledge extract** : Extraire données de docs/fixKnowledge vers docs/ ; supprimer docs/fixKnowledge.
|
||||||
|
|
||||||
|
**docs/ cleanup** : Réunir et optimiser en max 20 documents ; supprimer infos fausses/obsolètes ; ventiler dans doc existante (pas de FEATURES.md ni FIXKNOWLEDGE.md).
|
||||||
|
**Plan** : `.cursor/plans/workflow-docupdate.plan.md`
|
||||||
|
**Commande** : `/docupdate`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 Workflow d'audit sécurité
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/audit-security`.
|
||||||
|
|
||||||
|
**Processus** : Analyse surface d'attaque ; revue OWASP Top 10 ; évaluation risque ; recommandations ; checklist Security Headers.
|
||||||
|
|
||||||
|
**Format rapport** : [SEVERITY] Titre ; Location ; Description ; Impact ; Reproduction ; Remediation ; Références.
|
||||||
|
**Plan** : `.cursor/plans/workflow-audit-security.plan.md`
|
||||||
|
**Commande** : `/audit-security`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 Workflow lintit (qualité code)
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/lintit`.
|
||||||
|
|
||||||
|
**Processus** : Vérifier règles qualité ; liste actions à mener. Couvre : analyse préalable, non-duplication, généricité, design patterns, gestion erreurs, interdiction fallback, interdiction facilités IA ; corriger erreurs et warnings lint sans contournement.
|
||||||
|
**Plan** : `.cursor/plans/workflow-lintit.plan.md`
|
||||||
|
**Commande** : `/lintit`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 Workflow banch-align
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/banch-align <env>`.
|
||||||
|
|
||||||
|
**Processus** : Aligner test, pprod, prod (sauf `<env>`) sur la branche de `<env>` sans modifier `<env>`. Merge en conflit : `<env>` prioritaire. Stash si conflits. Vérifier 30 derniers commits équivalents.
|
||||||
|
|
||||||
|
**Résultat** : origin/test, origin/pprod, origin/prod au même commit ; branches locales à jour.
|
||||||
|
**Plan** : `.cursor/plans/workflow-banch-align.plan.md`
|
||||||
|
**Commande** : `/banch-align <env>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9 Workflow banch-align-update
|
||||||
|
|
||||||
|
**Déclencheur** : Commande `/banch-align-update <env>`.
|
||||||
|
|
||||||
|
**Processus** : Aligner branche actuelle sur `<env>` sans modifier `<env>`. Stash avant ; liste modifications ; confirmation. Branche locale alignée sur commits de `<env>` ; reste sur même branche.
|
||||||
|
**Plan** : `.cursor/plans/workflow-banch-align-update.plan.md`
|
||||||
|
**Commande** : `/banch-align-update <env>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Plans Cursor (déclenchables)
|
||||||
|
|
||||||
|
Les workflows sont migrés en plans Cursor stockés au niveau utilisateur (`~/.cursor/plans/`) et déclenchables via les commandes projet `.cursor/commands/`.
|
||||||
|
|
||||||
|
### Format Cursor attendu
|
||||||
|
|
||||||
|
- **Frontmatter YAML** : `name`, `overview`, `todos` (id, content, status), `isProject: false`
|
||||||
|
- **Corps Markdown** : phases, étapes, liens vers fichiers `[fichier](chemin/relatif)`
|
||||||
|
|
||||||
|
### Liste des plans
|
||||||
|
|
||||||
|
| Plan | Commande | Description |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| workflow-cloture-evolution.plan.md | /cloture-evolution | Clôture évolution/correction |
|
||||||
|
| workflow-deploy.plan.md | /deploy | Déploiement test/pprod/prod |
|
||||||
|
| workflow-fix-lint.plan.md | /fix-lint | Correction lint |
|
||||||
|
| workflow-pousse.plan.md | /pousse | Commit et push |
|
||||||
|
| workflow-docupdate.plan.md | /docupdate | Mise à jour documentation |
|
||||||
|
| workflow-audit-security.plan.md | /audit-security | Audit sécurité |
|
||||||
|
| workflow-lintit.plan.md | /lintit | Qualité code |
|
||||||
|
| workflow-banch-align.plan.md | /banch-align | Aligner branches |
|
||||||
|
| workflow-banch-align-update.plan.md | /banch-align-update | Aligner branche actuelle |
|
||||||
|
|
||||||
|
### Déclenchement
|
||||||
|
|
||||||
|
- **Commande** : Taper `/` dans le chat puis le nom (ex. `/cloture-evolution`, `/deploy test`)
|
||||||
|
- **Plan mode** : Shift+Tab pour créer un plan ; ouvrir `~/.cursor/plans/workflow-*.plan.md` et cliquer « Build » pour exécuter
|
||||||
|
- **Priorité** : Les commandes projet (`.cursor/commands/`) ont priorité sur les commandes globales (`~/.cursor/commands/`)
|
||||||
|
|
||||||
|
**Référence** : .cursor/plans/README.md, ~/.cursor/plans/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Composants : Règles
|
||||||
|
|
||||||
|
**Emplacement** : `.cursor/rules/*.mdc` (projet).
|
||||||
|
|
||||||
|
| Règle | Description | alwaysApply |
|
||||||
|
|-------|-------------|-------------|
|
||||||
|
| rules.mdc | Règles générales (langues, restrictions, processus, qualité, doc, déploiement) | true |
|
||||||
|
| cloture-evolution.mdc | Workflow de clôture à la fin de chaque évolution/correction | true |
|
||||||
|
| development-autonomy.mdc | Références user stories, patterns, qualité, sécurité, tests | true |
|
||||||
|
| error-fixing.mdc | Corriger l'erreur et jamais contourner | true |
|
||||||
|
| investigation-analysis.mdc | Investigation via logs, données ; services host-native | true |
|
||||||
|
|
||||||
|
**Format** : YAML frontmatter (description, globs, alwaysApply) + contenu markdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Composants : Subagents
|
||||||
|
|
||||||
|
**Emplacement** : `~/.cursor/agents/*.md` (utilisateur).
|
||||||
|
|
||||||
|
| Subagent | Description | is_background | mcp_task |
|
||||||
|
|----------|-------------|---------------|----------|
|
||||||
|
| deploy | Exécute `./deploy/scripts_v2/deploy.sh <env>` (script local, déploie à distance) | false | subagent_type="deploy" |
|
||||||
|
| fix-lint | Corrige erreurs lint backend, frontend, ressources | true | Non fonctionnel (utiliser generalPurpose) |
|
||||||
|
|
||||||
|
**Invocation** : `/deploy`, `/fix-lint` ou « Utilise le subagent X » dans le chat.
|
||||||
|
|
||||||
|
**Tâches de fond** : fix-lint avec `is_background: true` retourne immédiatement ; écrit état dans `~/.cursor/subagents/` ; reprise possible via ID.
|
||||||
|
|
||||||
|
**Référence** : ~/.cursor/agents/README.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Composants : Commandes
|
||||||
|
|
||||||
|
**Emplacements** : `.cursor/commands/*.md` (projet, priorité) ; `~/.cursor/commands/*.md` (global).
|
||||||
|
|
||||||
|
Les commandes projet invoquent les plans `.cursor/plans/*.plan.md`.
|
||||||
|
|
||||||
|
| Commande | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| deploy | Déploie sur test/pprod/prod |
|
||||||
|
| fix-lint | Corrige erreurs lint |
|
||||||
|
| lintit | Vérifie règles qualité, liste actions |
|
||||||
|
| docupdate | Mise à jour et rationalisation docs |
|
||||||
|
| pousse | Commit + push avec format structuré |
|
||||||
|
| audit-security | Audit sécurité OWASP |
|
||||||
|
| banch-align | Aligne branches test/pprod/prod sur `<env>` |
|
||||||
|
| banch-align-update | Aligne branche actuelle sur `<env>` |
|
||||||
|
| science-redaction | Rédaction scientifique |
|
||||||
|
| scientifi-check | Vérification scientifique |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Composants : Skills
|
||||||
|
|
||||||
|
**Emplacement** : `~/.cursor/skills-cursor/*/SKILL.md` (utilisateur).
|
||||||
|
|
||||||
|
| Skill | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| create-rule | Créer règles Cursor (.mdc) |
|
||||||
|
| create-skill | Créer skills |
|
||||||
|
| create-subagent | Créer subagents |
|
||||||
|
| update-cursor-settings | Modifier settings.json |
|
||||||
|
| migrate-to-skills | Migration vers skills |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. MCP et outils
|
||||||
|
|
||||||
|
### MCP mcp_task
|
||||||
|
|
||||||
|
**Types disponibles** : generalPurpose, explore, deploy, fix-lint.
|
||||||
|
|
||||||
|
| Type | Usage | Remarque |
|
||||||
|
|------|-------|----------|
|
||||||
|
| generalPurpose | Tâches complexes, recherche, corrections | Toujours fonctionnel |
|
||||||
|
| explore | Exploration codebase rapide | Niveaux : quick, medium, very thorough |
|
||||||
|
| deploy | Déploiement test/pprod/prod | Lance deploy.sh |
|
||||||
|
| fix-lint | Correction lint | Non fonctionnel ; utiliser generalPurpose |
|
||||||
|
|
||||||
|
### Outils MCP browser
|
||||||
|
|
||||||
|
Tests E2E : navigation, snapshot, click, type, fill, etc. Référence : user_stories/.
|
||||||
|
|
||||||
|
### Outils projet
|
||||||
|
|
||||||
|
| Outil | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| npm run lint | ESLint |
|
||||||
|
| npm run typecheck | TypeScript (front) |
|
||||||
|
| npx tsc --noEmit | TypeScript (back, ressources) |
|
||||||
|
| npm run lint:markdown | Markdown (MD032, MD033, MD040) |
|
||||||
|
| ReadLints | Diagnostics IDE |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Usage
|
||||||
|
|
||||||
|
### 9.1 Quand utiliser quoi
|
||||||
|
|
||||||
|
| Situation | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| Démarrer une évolution ou correction | Formuler la demande ; l'IA applique les règles (rules.mdc, development-autonomy) |
|
||||||
|
| Fin d'évolution ou correction | `/cloture-evolution` ou workflow automatique (cloture-evolution.mdc) ; l'humain valide le déploiement si demandé |
|
||||||
|
| Erreurs de lint à corriger | `/fix-lint` ou « Corrige les erreurs de lint » |
|
||||||
|
| Déployer sur un environnement | `/deploy test` (ou pprod, prod) ; valider avant exécution |
|
||||||
|
| Commiter et pousser | `/pousse` après validation du message de commit |
|
||||||
|
| Vérifier qualité du code | `/lintit` |
|
||||||
|
| Audit sécurité | `/audit-security` |
|
||||||
|
| Mise à jour documentation | `/docupdate` |
|
||||||
|
| Aligner les branches | `/banch-align <env>` ou `/banch-align-update <env>` |
|
||||||
|
| Créer une règle, skill ou subagent | Demander à l'IA d'utiliser le skill create-rule, create-skill ou create-subagent |
|
||||||
|
|
||||||
|
### 9.2 Invocation des commandes
|
||||||
|
|
||||||
|
- **Slash** : `/deploy`, `/fix-lint`, `/pousse`, etc. dans le chat Cursor.
|
||||||
|
- **Phrase** : « Déploie sur test », « Corrige les erreurs de lint », « Commit et push ».
|
||||||
|
- **Subagents** : « Utilise le subagent fix-lint » ou invocation automatique si la description correspond.
|
||||||
|
- **Plans** : Les commandes projet (`/cloture-evolution`, `/deploy`, etc.) chargent et exécutent les plans correspondants.
|
||||||
|
|
||||||
|
### 9.3 Règles appliquées automatiquement
|
||||||
|
|
||||||
|
Les règles avec `alwaysApply: true` sont actives à chaque conversation : rules.mdc, cloture-evolution.mdc, development-autonomy.mdc, error-fixing.mdc, investigation-analysis.mdc. Aucune action requise.
|
||||||
|
|
||||||
|
### 9.4 Validation requise
|
||||||
|
|
||||||
|
- **Déploiement** : L'IA demande validation avant d'exécuter deploy.sh.
|
||||||
|
- **Commits** : L'utilisateur valide le message (format structuré) avant exécution de pousse.
|
||||||
|
- **Modifications sensibles** : Modèle de données, architecture, nouvelles dépendances : validation avant implémentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Protocole de développement
|
||||||
|
|
||||||
|
Ce protocole décrit comment l'humain utilise l'outil IA pour développer sur le projet.
|
||||||
|
|
||||||
|
### 10.1 Avant de commencer
|
||||||
|
|
||||||
|
1. Consulter `user_stories/INDEX.md` pour le contexte des parcours (si présent).
|
||||||
|
2. Consulter `docs/CODE_STANDARDS.md` et `docs/CODE_SECURITY.md` pour les contraintes.
|
||||||
|
3. S'assurer d'être sur la bonne branche (test, pprod ou prod selon l'environnement cible).
|
||||||
|
|
||||||
|
### 10.2 Pendant le développement
|
||||||
|
|
||||||
|
1. **Formuler la demande** : Décrire l'évolution ou la correction de façon précise (objectif, périmètre, contraintes).
|
||||||
|
2. **Valider les choix** : Si l'IA propose une solution (architecture, design, modèle de données), valider avant implémentation.
|
||||||
|
3. **Suivre les boucles** : L'IA applique le workflow de clôture à la fin ; l'humain peut demander d'arrêter une boucle ou de prioriser une étape.
|
||||||
|
4. **Investiguer** : En cas de bug, demander une investigation (logs, données, doc) ; l'IA utilise investigation-analysis.mdc.
|
||||||
|
5. **Concurrence des actions** : Voir [10.2.1 Concurrence des actions](#1021-concurrence-des-actions).
|
||||||
|
|
||||||
|
#### 10.2.1 Concurrence des actions
|
||||||
|
|
||||||
|
Les actions longues (lint, fix-lint, déploiement, typecheck, turbopack) ne doivent pas se chevaucher. Règles :
|
||||||
|
|
||||||
|
| Priorité | Action | Comportement |
|
||||||
|
|----------|--------|--------------|
|
||||||
|
| 1 | Déploiement | Arrêter proprement toute action en cours (lint, fix-lint, typecheck, turbopack) avant de lancer le déploiement. Le déploiement s'exécute seul. |
|
||||||
|
| 2 | Lint / fix-lint | Ne pas lancer si un déploiement est en cours. Arrêter (Ctrl+C ou annulation subagent) avant de lancer un déploiement. |
|
||||||
|
| 3 | Typecheck / turbopack | S'exécutent entre les lots de fix-lint ; ne pas lancer en parallèle d'un déploiement. |
|
||||||
|
|
||||||
|
**Avant de lancer un déploiement** : Le script `deploy/scripts_v2/deploy.sh` exécute un hook `hooks/pre-deploy.sh` qui arrête (SIGTERM) les processus concurrents dont le cwd est dans le projet (eslint, tsc, turbopack). Si des processus restent hors projet, le script bloque. Après déploiement, un message suggère de relancer /fix-lint si nécessaire.
|
||||||
|
|
||||||
|
**Si une action est lancée alors qu'une autre tourne** : Arrêter proprement l'action en cours (Ctrl+C pour processus foreground ; annulation du subagent pour fix-lint) avant de démarrer la nouvelle.
|
||||||
|
|
||||||
|
**Une action à la fois** : Ne pas lancer lint, fix-lint, déploiement ou build/typecheck en parallèle dans des terminaux distincts sans coordination.
|
||||||
|
|
||||||
|
**Contournement** : `DEPLOY_SKIP_CONCURRENCY_CHECK=1` désactive le hook pre-deploy (CI, cas documentés).
|
||||||
|
|
||||||
|
### 10.3 À la fin d'une évolution ou correction
|
||||||
|
|
||||||
|
1. **Workflow de clôture** : L'IA exécute automatiquement (tests, types, reproductions, factorisation, lint, sécurité, doc).
|
||||||
|
2. **Validation déploiement** : Si déploiement demandé, valider explicitement avant exécution.
|
||||||
|
3. **Commit** : Valider le message de commit proposé ; utiliser `/pousse` pour commiter et pousser.
|
||||||
|
4. **Vérification** : Consulter les logs de déploiement si applicable ; vérifier que la doc est à jour.
|
||||||
|
|
||||||
|
### 10.4 Bonnes pratiques
|
||||||
|
|
||||||
|
- **Une demande à la fois** : Éviter les demandes multiples non liées dans un même message.
|
||||||
|
- **Précision** : Donner le contexte (fichier, fonction, user story) pour limiter les allers-retours.
|
||||||
|
- **Validation** : Ne pas valider un déploiement ou un commit sans avoir vérifié les changements.
|
||||||
|
- **Documentation** : Signaler si la doc existante est obsolète ou incomplète.
|
||||||
|
- **Pas de contournement** : Refuser toute proposition de désactiver des règles, d'ignorer des erreurs ou de contourner un problème.
|
||||||
|
|
||||||
|
### 10.5 En cas de problème
|
||||||
|
|
||||||
|
- **L'IA contourne** : Rappeler « Corrige l'erreur et jamais contourner » (error-fixing.mdc).
|
||||||
|
- **Subagent fix-lint échoue** : Utiliser « Corrige les erreurs de lint avec generalPurpose » ou exécuter manuellement `npm run lint` et demander les corrections.
|
||||||
|
- **Déploiement en échec** : Analyser les logs ; ne pas relancer sans avoir identifié et corrigé la cause.
|
||||||
|
- **Règle trop stricte** : Documenter l'exception dans `docs/README.md` (consolidation opérationnelle) avec justification ; ne pas modifier les règles sans validation.
|
||||||
|
|
||||||
|
### 10.6 Ordre typique d'une session
|
||||||
|
|
||||||
|
1. Lire le contexte (user story, doc, code existant).
|
||||||
|
2. Formuler la demande d'évolution ou de correction.
|
||||||
|
3. Valider les propositions de l'IA.
|
||||||
|
4. Laisser l'IA implémenter et exécuter le workflow de clôture.
|
||||||
|
5. Valider déploiement si pertinent.
|
||||||
|
6. Valider et exécuter le commit (`/pousse`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Références** : docs/README.md, docs/CODE_STANDARDS.md, docs/DEPLOYMENT.md, CLAUDE.md.
|
||||||
171
projects/ia_dev/docs/agents-scripts-split.md
Normal file
171
projects/ia_dev/docs/agents-scripts-split.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# Répartition agent / script (agents lançant des scripts)
|
||||||
|
|
||||||
|
**Objectif :** Exploiter au mieux la valeur ajoutée de l'agent (orchestration, contenu structuré, règles projet) et du script (déterminisme, reproductibilité, exécutable sans LLM).
|
||||||
|
|
||||||
|
## Principes
|
||||||
|
|
||||||
|
| Rôle | Agent | Script |
|
||||||
|
|------|--------|--------|
|
||||||
|
| **Orchestration** | Ordre des étapes, invocation d'autres agents (push, docupdate, etc.) | Une seule responsabilité par script |
|
||||||
|
| **Contenu** | Message de commit structuré, CHANGELOG, décisions métier | Pas de génération de texte |
|
||||||
|
| **Règles projet** | Clôture (cloture-evolution.mdc), 5 sub-agents, docupdate | Contraintes techniques (auteur, chemins sensibles, branche) |
|
||||||
|
| **Déterminisme** | — | Vérifications git, chemins, options reproductibles |
|
||||||
|
| **Réutilisabilité** | Contexte Cursor | CLI / humain / CI sans agent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. branch-align-by-script-from-test
|
||||||
|
|
||||||
|
**Agent aujourd'hui :**
|
||||||
|
|
||||||
|
- Horodatage, clôture
|
||||||
|
- `cd` racine projet
|
||||||
|
- Lancer push-by-script puis docupdate puis `./deploy/branch-align.sh <env>`
|
||||||
|
- Ne pas masquer la sortie
|
||||||
|
|
||||||
|
**Script branch-align.sh aujourd'hui :**
|
||||||
|
|
||||||
|
- Vérif repo git, argument env, branche courante = env
|
||||||
|
- Fetch, force-push with lease, alignement test/pprod/prod, vérifications
|
||||||
|
|
||||||
|
**Déplacé dans le script :**
|
||||||
|
|
||||||
|
- **Exécution depuis racine du dépôt :** si le script est invoqué hors racine, se ré-exécuter depuis `git rev-parse --show-toplevel` pour que `./deploy/branch-align.sh` soit utilisable depuis n'importe quel sous-dossier.
|
||||||
|
|
||||||
|
**Reste dans l'agent :**
|
||||||
|
|
||||||
|
- Ordre push → docupdate → branch-align
|
||||||
|
- Invocation des agents push-by-script et docupdate
|
||||||
|
- Clôture complète
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. change-to-all-branches
|
||||||
|
|
||||||
|
**Agent aujourd'hui :**
|
||||||
|
|
||||||
|
- Vérifier branche = test (sinon retour 1)
|
||||||
|
- Lancer /push-by-script, /branch-align-by-script-from-test, /deploy-by-script
|
||||||
|
- Retour 0
|
||||||
|
|
||||||
|
**Déplacé dans un script :**
|
||||||
|
|
||||||
|
- **Nouveau script `deploy/change-to-all-branches.sh` :**
|
||||||
|
- Vérifier que la branche courante est `test` (sinon exit 1)
|
||||||
|
- Exécuter `./deploy/branch-align.sh test`
|
||||||
|
- Exécuter `./deploy/scripts_v2/deploy.sh test --import-v1 --skipSetupHost`
|
||||||
|
- À lancer depuis la racine du dépôt (le script peut faire `cd` vers la racine git au démarrage)
|
||||||
|
|
||||||
|
**Reste dans l'agent :**
|
||||||
|
|
||||||
|
- Lancer /push-by-script (message de commit fourni par l'agent)
|
||||||
|
- Puis lancer le script `deploy/change-to-all-branches.sh` (alignement + déploiement)
|
||||||
|
- Pas d'appel à branch-align-by-script-from-test ni deploy-by-script séparés : un seul script enchaîne align + deploy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. deploy-by-script
|
||||||
|
|
||||||
|
**Agent aujourd'hui :**
|
||||||
|
|
||||||
|
- Vérifier le suivi des branches (main/test/pprod/prod → origin/…), corriger avec `git branch --set-upstream-to`
|
||||||
|
- S'assurer que la branche courante est à jour : `git fetch origin` puis `git reset --hard origin/$(git branch --show-current)`
|
||||||
|
- `mkdir -p logs`, puis exécuter le script avec `tee logs/deploy_*.log`
|
||||||
|
- Ne pas masquer la sortie
|
||||||
|
|
||||||
|
**Script deploy.sh aujourd'hui :**
|
||||||
|
|
||||||
|
- Env, options, dotenv, vérifs git (DEPLOY_GIT_REMOTE, synchro), déploiement
|
||||||
|
|
||||||
|
**Déplacé dans le script :**
|
||||||
|
|
||||||
|
- **Pré-vérification du suivi des branches :** au début de `deploy.sh`, après `PROJECT_ROOT` et validation de `ENV` : `git fetch origin` puis pour chaque branche locale parmi main, test, pprod, prod définir l'upstream `origin/<branch>` si la branche existe. Pas de `reset --hard` dans le script (le sync avec `origin` reste à l'agent avant d'appeler le script).
|
||||||
|
|
||||||
|
**Reste dans l'agent :**
|
||||||
|
|
||||||
|
- S'assurer que la branche courante est à jour avec sa branche distante (`git fetch` puis `git reset --hard origin/$(git branch --show-current)`)
|
||||||
|
- Créer `logs/` et lancer le script avec `tee` vers un log daté
|
||||||
|
- Clôture complète
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. push-by-script
|
||||||
|
|
||||||
|
**Agent aujourd'hui :**
|
||||||
|
|
||||||
|
- Construire le message de commit (toutes les sections obligatoires)
|
||||||
|
- Mettre à jour CHANGELOG.md
|
||||||
|
- Mettre à jour VERSION (incrément sous-sous-version)
|
||||||
|
- Exécuter `./deploy/pousse.sh` avec le message sur STDIN
|
||||||
|
|
||||||
|
**Script pousse.sh aujourd'hui :**
|
||||||
|
|
||||||
|
- Lire le message sur STDIN, `git add -A`, vérifications (auteur, chemins sensibles, branche distante), commit, push
|
||||||
|
|
||||||
|
**Déplacé dans le script :**
|
||||||
|
|
||||||
|
- **Option `--bump-version` :** avant le `git add -A`, lire le fichier `VERSION` à la racine du dépôt, incrémenter le troisième segment (patch), réécrire `VERSION`. Ensuite `git add -A` inclura le fichier modifié. L'agent n'a plus à éditer VERSION à la main ; il fournit le message (en mentionnant la nouvelle sous-sous-version si besoin) et peut appeler `pousse.sh --bump-version`.
|
||||||
|
|
||||||
|
**Reste dans l'agent :**
|
||||||
|
|
||||||
|
- Construction du message de commit (toutes les sections)
|
||||||
|
- Mise à jour de CHANGELOG.md
|
||||||
|
- Invocation de `./deploy/pousse.sh` (avec `--bump-version` si souhaité)
|
||||||
|
- Clôture complète
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Synthèse des implémentations
|
||||||
|
|
||||||
|
| Fichier | Modification |
|
||||||
|
|---------|--------------|
|
||||||
|
| `deploy/branch-align.sh` | Ré-exécution depuis la racine git si nécessaire |
|
||||||
|
| `deploy/scripts_v2/deploy.sh` | Suivi branches origin ; **par défaut** sync (--sync-origin) et log (--log-to-dir logs) ; --no-sync-origin, --no-log pour désactiver |
|
||||||
|
| `deploy/pousse.sh` | **Ré-exéc depuis racine** si besoin ; option --bump-version ; build check (ressources, backend, frontend) avant staging |
|
||||||
|
| `deploy/change-to-all-branches.sh` | Vérif branche test, branch-align.sh test, deploy.sh test --import-v1 --skipSetupHost --no-sync-origin (log par défaut) |
|
||||||
|
| Agents .cursor/agents/*.md | Adapter les consignes pour utiliser les nouvelles options/scripts et alléger les étapes redondantes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deuxième passe (agent = contrôle / script = exécution)
|
||||||
|
|
||||||
|
**Principe :** L'agent assure vérifications, corrections, relances, synthèses et textes. Le script assure l'exécution et l'orchestration déterministe.
|
||||||
|
|
||||||
|
### deploy-by-script / deploy.sh
|
||||||
|
|
||||||
|
- **Dans le script :** option `--sync-origin` (git fetch + reset --hard origin/current) ; option `--log-to-dir <dir>` (création du répertoire et tee vers un fichier daté). L'agent n'exécute plus ces commandes ni tee à la main.
|
||||||
|
- **Dans l'agent :** lancer le script avec `--sync-origin --log-to-dir logs`, contrôler la sortie et le code de retour.
|
||||||
|
|
||||||
|
### push-by-script / pousse.sh
|
||||||
|
|
||||||
|
- **Dans le script :** build check (npm run build pour les répertoires listés dans `projects/<id>/conf.json` build_dirs) avant staging ; en cas d'échec, sortie en erreur sans commit ni push.
|
||||||
|
- **Dans l'agent :** construire le message, mettre à jour CHANGELOG, lancer le script, contrôler sortie et code de retour.
|
||||||
|
|
||||||
|
### Tous les agents
|
||||||
|
|
||||||
|
- Chaque agent précise en en-tête **Rôle de l'agent** et **Rôle du script**.
|
||||||
|
- Consignes redondantes retirées ou raccourcies au profit du contrôle (sortie non masquée, code de retour, rapport succès ou erreur).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troisième passe (priorité : exécution standardisée dans le script)
|
||||||
|
|
||||||
|
**Principe :** Tout ce qui peut être standardisé est dans le script pour garantir la même exécution à chaque run.
|
||||||
|
|
||||||
|
### deploy.sh
|
||||||
|
|
||||||
|
- **Par défaut :** `SYNC_ORIGIN=true`, `LOG_TO_DIR="logs"`. Chaque invocation fait sync avec origin et tee vers `logs/deploy_YYYYMMDD_HHMMSS.log` sans que l'agent ait à passer des options.
|
||||||
|
- **Désactivation :** `--no-sync-origin` et `--no-log`. L'agent invoque simplement `deploy.sh <env> --import-v1 --skipSetupHost`.
|
||||||
|
|
||||||
|
### pousse.sh
|
||||||
|
|
||||||
|
- **Ré-exécution depuis la racine :** si le script est appelé hors racine du dépôt, il se ré-exécute depuis `git rev-parse --show-toplevel`. Exécution toujours depuis la racine (comme branch-align.sh et change-to-all-branches.sh).
|
||||||
|
|
||||||
|
### change-to-all-branches.sh
|
||||||
|
|
||||||
|
- Appelle `deploy.sh test --import-v1 --skipSetupHost --no-sync-origin` (après push, la branche est déjà à jour ; log dans logs/ reste le défaut).
|
||||||
|
|
||||||
|
### Agents
|
||||||
|
|
||||||
|
- deploy-by-script : une seule commande sans options sync/log (comportement par défaut).
|
||||||
|
- push-by-script : le script peut être appelé depuis n'importe quel sous-dossier.
|
||||||
40
projects/lecoffreio/conf.json
Normal file
40
projects/lecoffreio/conf.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "lecoffreio",
|
||||||
|
"name": "Lecoffre.io",
|
||||||
|
"project_path": "/home/desk/code/lecoffre_ng_test/deploy",
|
||||||
|
"build_dirs": [
|
||||||
|
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-ressources-dev",
|
||||||
|
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-back-main",
|
||||||
|
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-front-main"
|
||||||
|
],
|
||||||
|
"deploy": {
|
||||||
|
"scripts_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2",
|
||||||
|
"deploy_script_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2/deploy.sh",
|
||||||
|
"secrets_path": "/home/desk/code/lecoffre_ng_test/.secrets"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"package_json_paths": [
|
||||||
|
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-back-main/package.json",
|
||||||
|
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-front-main/package.json"
|
||||||
|
],
|
||||||
|
"splash_app_name": "LeCoffre.io"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"email": "ai.support.lecoffreio@4nkweb.com",
|
||||||
|
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"wiki_url": "https://git.4nkweb.com/4nk/lecoffre_ng/wiki",
|
||||||
|
"token_file": ".secrets/gitea-issues/token"
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"ticketing_url": "https://git.4nkweb.com/4nk/lecoffre_ng/issues",
|
||||||
|
"authorized_emails": {
|
||||||
|
"to": "ai.support.lecoffreio@4nkweb.com",
|
||||||
|
"from": [
|
||||||
|
"laurence@lecoffre.io",
|
||||||
|
"gwendal@lecoffre.io"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
xjMEYSXwVBYJKwYBBAHaRw8BAQdAsulrfkUdJQUxil8hC0AsehMTLq5l8TKx
|
||||||
|
iPOW807Y2u/NKW5pY29sYXMuY2FudHVAcG0ubWUgPG5pY29sYXMuY2FudHVA
|
||||||
|
cG0ubWU+wo8EEBYKACAFAmEl8FQGCwkHCAMCBBUICgIEFgIBAAIZAQIbAwIe
|
||||||
|
AQAhCRBxlbYDS4nNZhYhBK/x7PS2N5gMcqLnmHGVtgNLic1mvpIBAKq7Qfem
|
||||||
|
eEwWmX6unDM/hpnIl2CwDz+mmvaqHHZ/GXjwAQCAvIX82AhcjmnT90VKB5gP
|
||||||
|
P3xanAdUQLNjjsnAtcogDc44BGEl8FQSCisGAQQBl1UBBQEBB0DyQexAoEWM
|
||||||
|
pF7UOC8XJlz1BvnLhBitn596XSZg4exBSQMBCAfCeAQYFggACQUCYSXwVAIb
|
||||||
|
DAAhCRBxlbYDS4nNZhYhBK/x7PS2N5gMcqLnmHGVtgNLic1mQ0AA/1L7m+Cs
|
||||||
|
ZJLWnVxnn5Cdd9b6FZpAHK8f+LcCQ8euYudJAQDiUeKm8KZUvvYOGIvJuisG
|
||||||
|
YHDv7GY5mNCR1JdWaR1rDQ==
|
||||||
|
=TDqL
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"type": "incoming",
|
||||||
|
"id": "af28dfa2",
|
||||||
|
"message_id": "<XImHtfSZSMJo1z1dqMvd19K5CPkEJ6q6-3Ky88Muiy5c_ED4mcc6JeLT-RY1Lt9T5b57Ce3GEvrrP6_kUZ0cToHtLKF2x324qbZDyLjqYG8=@pm.me>",
|
||||||
|
"from": "nicolas.cantu@pm.me",
|
||||||
|
"to": [
|
||||||
|
"\"ai.support.lecoffreio@4nkweb.com\" <ai.support.lecoffreio@4nkweb.com>"
|
||||||
|
],
|
||||||
|
"subject": "Présente le projet",
|
||||||
|
"date": "Sat, 14 Mar 2026 13:41:28 +0000",
|
||||||
|
"body": "",
|
||||||
|
"references": [
|
||||||
|
"<6tpKkxA0z1VR1XTFT_L-rtTce3HmvVxnicdFfSgp0wx0-4ptNS6uaTXDXEa2Z0Po0URmrTRSwpprPwAxEeFacQ==@protonmail.internalid>"
|
||||||
|
],
|
||||||
|
"in_reply_to": null,
|
||||||
|
"uid": "7903",
|
||||||
|
"created_at": "2026-03-14T13:42:53Z",
|
||||||
|
"issue_number": null,
|
||||||
|
"status": "responded",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"filename": "publickey - nicolas.cantu@pm.me -\r\n 0xAFF1ECF4.asc",
|
||||||
|
"path": "2026-03-14T134128.af28dfa2.nicolas.cantu_pm.me.d/0_publickey_-_nicolas.cantu_pm.me_-___0xAFF1ECF4.asc",
|
||||||
|
"content_type": "application/pgp-keys",
|
||||||
|
"size": 653
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"response": {
|
||||||
|
"in_reply_to_message_id": "<XImHtfSZSMJo1z1dqMvd19K5CPkEJ6q6-3Ky88Muiy5c_ED4mcc6JeLT-RY1Lt9T5b57Ce3GEvrrP6_kUZ0cToHtLKF2x324qbZDyLjqYG8=@pm.me>",
|
||||||
|
"to": "nicolas.cantu@pm.me",
|
||||||
|
"subject": "Re: Présente le projet",
|
||||||
|
"body": "Le projet LeCoffre.io est une application de coffre-fort numérique (dépôt lecoffre_ng). La documentation technique est sur le wiki du dépôt Gitea (https://git.4nkweb.com/4nk/lecoffre_ng/wiki). Les déploiements test, préproduction et production passent par les scripts deploy/ ; le ticketing et le support IA par les scripts gitea-issues/ dans ia_dev.",
|
||||||
|
"sent_at": "2026-03-14T13:43:30Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
841
projects/lecoffreio/docs/ANCRAGE_COMPLETE.md
Normal file
841
projects/lecoffreio/docs/ANCRAGE_COMPLETE.md
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
# Documentation Complète de l'Ancrage Blockchain - LeCoffre.io
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 2025-11-24
|
||||||
|
**Version** : 3.1.2
|
||||||
|
|
||||||
|
Ce document consolide toute la documentation relative à l'ancrage blockchain des documents et dossiers dans LeCoffre.io.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Table des Matières
|
||||||
|
|
||||||
|
1. [Vue d'Ensemble](#1-vue-densemble)
|
||||||
|
2. [Architecture V3](#2-architecture-v3)
|
||||||
|
3. [Processus d'Ancrage](#3-processus-dancrage)
|
||||||
|
4. [Certificats et ZIP](#4-certificats-et-zip)
|
||||||
|
5. [Vérification et Statuts](#5-vérification-et-statuts)
|
||||||
|
6. [Scripts et Maintenance](#6-scripts-et-maintenance)
|
||||||
|
7. [Troubleshooting](#7-troubleshooting)
|
||||||
|
8. [Références](#8-références)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vue d'Ensemble
|
||||||
|
|
||||||
|
### Évolution des Versions
|
||||||
|
|
||||||
|
| Version | Blockchain | Périmètre | Déclenchement | Filigrane | Fallback |
|
||||||
|
|---------|-----------|-----------|---------------|-----------|----------|
|
||||||
|
| **V1** | Tezos | Dossiers uniquement | Manuel | ❌ Non | ⚠️ Lecture seule |
|
||||||
|
| **V2** | Bitcoin Signet | Docs + Dossiers | Auto | ❌ Non | - |
|
||||||
|
| **V3** | Bitcoin Signet | Docs + Dossiers | Auto | ✅ Oui | ✅ Tezos masqué |
|
||||||
|
|
||||||
|
### Nouveautés V3
|
||||||
|
|
||||||
|
1. **Filigrane automatique** : "lecoffre.io" sur tous documents
|
||||||
|
2. **Double version** : Original + filigrané (conservés en BDD)
|
||||||
|
3. **ZIP enrichi** : Originaux + filigranés + certificats + preuves JSON
|
||||||
|
4. **Preuves structurées** : JSON standardisé pour vérification externe
|
||||||
|
5. **Ancrage dossier** : Hash du ZIP complet (pas seulement Merkle tree)
|
||||||
|
6. **Fallback Tezos** : Accès masqué aux anciens ancrages (lecture seule)
|
||||||
|
|
||||||
|
### Principes Fondamentaux
|
||||||
|
|
||||||
|
- **Ancrage immédiat** : L'ancrage est déclenché automatiquement lors de la validation d'un document
|
||||||
|
- **Hash filigrané** : Seule la version filigranée est ancrée sur blockchain
|
||||||
|
- **Double hash** : Hash original et hash filigrané sont stockés pour vérification
|
||||||
|
- **Preuve on-chain** : Le `tx_id` (transaction ID Bitcoin) est la seule preuve fiable d'ancrage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture V3
|
||||||
|
|
||||||
|
### Services Backend
|
||||||
|
|
||||||
|
#### WatermarkService
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/services/common/WatermarkService/WatermarkService.ts`
|
||||||
|
|
||||||
|
**Responsabilités** :
|
||||||
|
|
||||||
|
- Conversion documents → PDF (si nécessaire)
|
||||||
|
- Ajout filigrane "lecoffre.io" (diagonal, 15% opacité)
|
||||||
|
- Upload version filigranée vers S3/IPFS
|
||||||
|
- Gestion formats : PDF, images, Word, PowerPoint
|
||||||
|
|
||||||
|
**Méthodes principales** :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async addWatermarkAndUpload(fileUid: string): Promise<{
|
||||||
|
watermarkedS3Key: string;
|
||||||
|
watermarkedSize: number;
|
||||||
|
watermarkedMimetype: string;
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DocumentAnchorsService
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/services/notary/DocumentAnchorsService/DocumentAnchorsService.ts`
|
||||||
|
|
||||||
|
**Workflow d'ancrage** :
|
||||||
|
|
||||||
|
1. Vérification ancrage Tezos existant (fallback)
|
||||||
|
2. Génération version filigranée si absente
|
||||||
|
3. Calcul hash version filigranée
|
||||||
|
4. Vérification ancrage Signet existant
|
||||||
|
5. Création ancrage `QUEUED`
|
||||||
|
6. Ancrage blockchain Bitcoin Signet
|
||||||
|
7. Génération `proof_data` (incluant fallback Tezos)
|
||||||
|
8. Mise à jour ancrage avec `tx_id`
|
||||||
|
|
||||||
|
**Méthodes principales** :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async anchorDocument(documentUid: string, anchoredByUid?: string): Promise<DocumentAnchors>
|
||||||
|
async getByDocumentUid(documentUid: string): Promise<DocumentAnchors | null>
|
||||||
|
async getByFileHash(fileHash: string): Promise<DocumentAnchors | null>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OfficeFolderAnchorsService
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/services/notary/OfficeFolderAnchorsService/OfficeFolderAnchorsService.ts`
|
||||||
|
|
||||||
|
**Workflow d'ancrage dossier** :
|
||||||
|
|
||||||
|
1. Récupération tous documents validés
|
||||||
|
2. Génération filigranes manquants
|
||||||
|
3. Génération ZIP complet dossier
|
||||||
|
4. Upload ZIP vers S3
|
||||||
|
5. Ancrage hash ZIP complet
|
||||||
|
6. Génération preuve dossier
|
||||||
|
|
||||||
|
**Structure ZIP** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dossier_12345678/
|
||||||
|
├── _LISEZ-MOI.txt
|
||||||
|
├── 01_originaux/
|
||||||
|
├── 02_filigranes/
|
||||||
|
├── 03_certificats/
|
||||||
|
└── 04_preuves/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ProofDataService
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/services/notary/ProofDataService/ProofDataService.ts`
|
||||||
|
|
||||||
|
**Responsabilités** :
|
||||||
|
|
||||||
|
- Génération JSON de preuve d'ancrage
|
||||||
|
- Format standard pour vérification externe
|
||||||
|
- Inclusion toutes données blockchain
|
||||||
|
|
||||||
|
**Structure JSON** :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "3.0.0",
|
||||||
|
"timestamp": "2025-10-29T16:30:00.000Z",
|
||||||
|
"document": {
|
||||||
|
"uid": "abc123...",
|
||||||
|
"name": "Acte de vente",
|
||||||
|
"original_hash": "e3b0c442...",
|
||||||
|
"watermarked_hash": "5feceb66..."
|
||||||
|
},
|
||||||
|
"anchor": {
|
||||||
|
"blockchain": "BITCOIN_SIGNET",
|
||||||
|
"status": "VERIFIED_ON_CHAIN",
|
||||||
|
"txid": "a1b2c3d4...",
|
||||||
|
"tx_link": "https://mempool.4nkweb.com/signet/tx/a1b2c3d4...",
|
||||||
|
"block_height": 123456,
|
||||||
|
"confirmations": 6
|
||||||
|
},
|
||||||
|
"tezos_fallback": {
|
||||||
|
"legacy": true,
|
||||||
|
"blockchain": "TEZOS",
|
||||||
|
"txid": "old_tezos_tx..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### BitcoinSignetService
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/services/common/BitcoinSignetService/BitcoinSignetService.ts`
|
||||||
|
|
||||||
|
**Responsabilités** :
|
||||||
|
|
||||||
|
- Ancrage hash sur Bitcoin Signet
|
||||||
|
- Vérification statut transaction
|
||||||
|
- Récupération métadonnées blockchain
|
||||||
|
|
||||||
|
**Méthodes principales** :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async anchorHash(hash: string): Promise<{
|
||||||
|
txid: string;
|
||||||
|
tx_hash: string;
|
||||||
|
tx_link: string;
|
||||||
|
}>
|
||||||
|
async verifyAnchor(txId: string): Promise<{
|
||||||
|
confirmed: boolean;
|
||||||
|
confirmations: number;
|
||||||
|
block_height: number;
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base de Données
|
||||||
|
|
||||||
|
#### Schéma Prisma
|
||||||
|
|
||||||
|
**Table `files`** :
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Files {
|
||||||
|
uid String @id @unique @default(uuid())
|
||||||
|
file_name String
|
||||||
|
s3_key String?
|
||||||
|
|
||||||
|
// V3 : Version filigranée
|
||||||
|
watermarked_s3_key String?
|
||||||
|
watermarked_at DateTime?
|
||||||
|
original_hash String? // Hash fichier original
|
||||||
|
|
||||||
|
document_uid String?
|
||||||
|
document Documents? @relation(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Table `document_anchors`** :
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model DocumentAnchors {
|
||||||
|
uid String @id @unique @default(uuid())
|
||||||
|
document_uid String @unique
|
||||||
|
|
||||||
|
// Hash de la version FILIGRANNÉE (V3)
|
||||||
|
file_hash String
|
||||||
|
|
||||||
|
blockchain EBlockchainName @default(BITCOIN_SIGNET)
|
||||||
|
status EAnchoringStatus @default(QUEUED)
|
||||||
|
|
||||||
|
// Données blockchain
|
||||||
|
tx_id String? // ⚠️ CRITIQUE : Seul indicateur fiable
|
||||||
|
tx_link String?
|
||||||
|
tx_hash String?
|
||||||
|
block_height Int?
|
||||||
|
block_time DateTime?
|
||||||
|
confirmations Int?
|
||||||
|
|
||||||
|
// V3 : Données de preuve (JSON)
|
||||||
|
proof_data Json?
|
||||||
|
|
||||||
|
anchored_at DateTime?
|
||||||
|
anchored_by_uid String?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Table `office_folder_anchors`** :
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model OfficeFolderAnchors {
|
||||||
|
uid String @id @unique @default(uuid())
|
||||||
|
|
||||||
|
// Hashs sources (Merkle tree)
|
||||||
|
root_hash String
|
||||||
|
|
||||||
|
// V3 : ZIP complet
|
||||||
|
zip_hash String?
|
||||||
|
zip_s3_key String?
|
||||||
|
zip_size Float?
|
||||||
|
|
||||||
|
blockchain EBlockchainName @default(BITCOIN_SIGNET)
|
||||||
|
status EAnchoringStatus @default(QUEUED)
|
||||||
|
|
||||||
|
// Données blockchain
|
||||||
|
tx_id String? // ⚠️ CRITIQUE : Seul indicateur fiable
|
||||||
|
tx_link String?
|
||||||
|
tx_hash String?
|
||||||
|
block_height Int?
|
||||||
|
confirmations Int?
|
||||||
|
|
||||||
|
// V3 : Données de preuve (JSON)
|
||||||
|
proof_data Json?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enums** :
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum EBlockchainName {
|
||||||
|
TEZOS // ⚠️ Conservé pour fallback (lecture seule)
|
||||||
|
BITCOIN_SIGNET
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EAnchoringStatus {
|
||||||
|
QUEUED // ⚠️ OBSOLÈTE : Non utilisé de manière fiable
|
||||||
|
ATTEMPTING // ⚠️ OBSOLÈTE : Non utilisé de manière fiable
|
||||||
|
VERIFIED_ON_CHAIN // ⚠️ OBSOLÈTE : Le système se base uniquement sur tx_id
|
||||||
|
FAILED
|
||||||
|
ABANDONED
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT** : Le champ `status` n'est plus utilisé de manière fiable. Le système vérifie uniquement la présence de `tx_id` :
|
||||||
|
|
||||||
|
- `tx_id` présent = Ancrage réussi
|
||||||
|
- `tx_id` absent = Ancrage non effectué
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Processus d'Ancrage
|
||||||
|
|
||||||
|
### Séquence de Traitement (Upload)
|
||||||
|
|
||||||
|
Pour **tous les documents** (Client, Tiers, Notaire invité, Notaire), la séquence est :
|
||||||
|
|
||||||
|
1. **Calcul hash original** (fichier original)
|
||||||
|
2. **Filigrane** (sur fichier original non chiffré)
|
||||||
|
3. **Calcul hash filigrané et ancrage blockchain** (hash filigrané, génération txid)
|
||||||
|
4. **Chiffrement** du document filigrané
|
||||||
|
5. **Upload IPFS** du document chiffré
|
||||||
|
6. **Stockage métadonnées** fichier (hash original + hash filigrané)
|
||||||
|
7. **Stockage certificat BDD** (document_anchor avec hash filigrané + hash original + txid)
|
||||||
|
|
||||||
|
### Ancrage Automatique lors de la Validation
|
||||||
|
|
||||||
|
**Déclenchement** : Lors de la validation d'un document via `PUT /api/v1/notary/documents/:uid`
|
||||||
|
|
||||||
|
**Fichier** : `lecoffre-back-main/src/app/api/notary/DocumentsController.ts`
|
||||||
|
|
||||||
|
**Workflow** :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (documentEntityUpdated.document_status === EDocumentStatus.VALIDATED && firstFileUid) {
|
||||||
|
// Ancrage automatique (asynchrone, non bloquant)
|
||||||
|
if (userUid) {
|
||||||
|
void this.documentAnchorsService
|
||||||
|
.anchorDocument(documentEntityUpdated.uid, userUid)
|
||||||
|
.then((anchor) => {
|
||||||
|
SafeLogger.info(`✅ Document ${documentEntityUpdated.uid} automatically anchored`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
SafeLogger.error(`❌ Auto-anchoring failed: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Points importants** :
|
||||||
|
|
||||||
|
- ✅ L'ancrage est **asynchrone** (non bloquant)
|
||||||
|
- ✅ L'ancrage est lancé en **arrière-plan**
|
||||||
|
- ✅ Si l'ancrage échoue, le document reste validé
|
||||||
|
|
||||||
|
### Ancrage Document
|
||||||
|
|
||||||
|
**Workflow détaillé** :
|
||||||
|
|
||||||
|
1. **Récupération document + fichier**
|
||||||
|
2. **Vérification ancrage Tezos existant** (fallback)
|
||||||
|
3. **Génération version filigranée** si absente
|
||||||
|
4. **Téléchargement version filigranée** et calcul hash
|
||||||
|
5. **Vérification ancrage Signet existant**
|
||||||
|
6. **Création ancrage `QUEUED`**
|
||||||
|
7. **Ancrage blockchain** via `BitcoinSignetService.anchorHash()`
|
||||||
|
8. **Génération `proof_data`** (incluant fallback Tezos)
|
||||||
|
9. **Mise à jour ancrage** avec `tx_id`, `tx_link`, `proof_data`
|
||||||
|
|
||||||
|
### Ancrage Dossier
|
||||||
|
|
||||||
|
**Workflow détaillé** :
|
||||||
|
|
||||||
|
1. **Récupération tous documents validés** du dossier
|
||||||
|
2. **Génération filigranes manquants** pour tous documents
|
||||||
|
3. **Génération ZIP complet** du dossier :
|
||||||
|
- Originaux (01_originaux/)
|
||||||
|
- Filigranés (02_filigranes/)
|
||||||
|
- Certificats (03_certificats/)
|
||||||
|
- Preuves JSON (04_preuves/)
|
||||||
|
4. **Upload ZIP vers S3**
|
||||||
|
5. **Ancrage hash ZIP complet** sur blockchain
|
||||||
|
6. **Génération preuve dossier**
|
||||||
|
7. **Création/Mise à jour ancrage dossier**
|
||||||
|
|
||||||
|
**Condition** : Le dossier doit être à **100% de complétion** (tous documents validés)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Certificats et ZIP
|
||||||
|
|
||||||
|
### Génération Automatique de Certificats
|
||||||
|
|
||||||
|
#### Téléchargement Individuel (Document Validé et Ancré)
|
||||||
|
|
||||||
|
**Endpoints** :
|
||||||
|
|
||||||
|
- Notaire : `GET /api/v1/notary/files/download/:uid`
|
||||||
|
- Client : `GET /api/v1/customer/files/download/:uid`
|
||||||
|
|
||||||
|
**Comportement automatique** : Pour les documents validés (`VALIDATED`) et ancrés avec statut `VERIFIED_ON_CHAIN`, le téléchargement génère **automatiquement** un ZIP contenant :
|
||||||
|
|
||||||
|
1. **PDF filigrané** : Document avec filigrane appliqué (suffixe `_aplc.pdf`)
|
||||||
|
2. **Certificat d'ancrage** : Certificat PDF Bitcoin Signet (préfixe `certificat_`)
|
||||||
|
|
||||||
|
**Exemple de ZIP** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
document_001_CNI_DUPONT_Jean_avec_certificat.zip
|
||||||
|
├── 001_CNI_DUPONT_Jean_aplc.pdf (PDF filigrané)
|
||||||
|
└── certificat_001_CNI_DUPONT_Jean.pdf (Certificat d'ancrage)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Téléchargement Multiple (ZIP de Fichiers)
|
||||||
|
|
||||||
|
**Service** : `ZipService.createZipFromFiles()`
|
||||||
|
|
||||||
|
**Workflow de recherche d'ancrage** :
|
||||||
|
|
||||||
|
1. **PRIORITÉ** : Recherche par `document_uid` (plus fiable)
|
||||||
|
2. **FALLBACK** : Si pas trouvé, recherche par hash du fichier
|
||||||
|
3. **Vérification statut** : Le certificat n'est généré que si `status === VERIFIED_ON_CHAIN`
|
||||||
|
|
||||||
|
#### Téléchargement Dossier Complet
|
||||||
|
|
||||||
|
**Service** : `OfficeFolderAnchorsService.generateFolderZip()`
|
||||||
|
|
||||||
|
**Structure ZIP** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dossier_12345678_Acte_de_vente/
|
||||||
|
├── _LISEZ-MOI.txt (Instructions vérification)
|
||||||
|
├── 01_originaux/
|
||||||
|
│ ├── acte_vente.pdf (Version originale)
|
||||||
|
│ └── compromis_vente.pdf
|
||||||
|
├── 02_filigranes/
|
||||||
|
│ ├── acte_vente.pdf (⚠️ VERSION ANCRÉE avec filigrane)
|
||||||
|
│ └── compromis_vente.pdf
|
||||||
|
├── 03_certificats/
|
||||||
|
│ ├── certificat_acte_vente.pdf (PDF certif blockchain)
|
||||||
|
│ └── certificat_compromis_vente.pdf
|
||||||
|
└── 04_preuves/
|
||||||
|
├── preuve_acte_vente.json (JSON avec hashs + TX)
|
||||||
|
└── preuve_compromis_vente.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contenu des Certificats PDF
|
||||||
|
|
||||||
|
**Informations affichées** :
|
||||||
|
|
||||||
|
1. **Hashs complets** :
|
||||||
|
- Empreinte SHA-256 (original) : Hash complet du document initial (64 caractères hex)
|
||||||
|
- Empreinte SHA-256 (filigrané) : Hash complet du document avec filigrane (64 caractères hex)
|
||||||
|
- Source : `proof_data.document.original_hash` et `proof_data.document.watermarked_hash`
|
||||||
|
|
||||||
|
2. **Lien de transaction** :
|
||||||
|
- Format : `https://mempool.4nkweb.com/fr/tx/<txid>`
|
||||||
|
- Construit automatiquement à partir de `anchor.tx_id` si disponible
|
||||||
|
- Fallback sur `anchor.tx_link` si `tx_id` absent
|
||||||
|
|
||||||
|
3. **Autres informations** :
|
||||||
|
- Identifiant unique du document
|
||||||
|
- Informations du dossier (numéro, nom)
|
||||||
|
- Informations de l'office notarial
|
||||||
|
- Type de document
|
||||||
|
- Déposant
|
||||||
|
- Validateur (UID)
|
||||||
|
- Nom de fichier normé
|
||||||
|
- Blockchain utilisée (Bitcoin Signet Notaires)
|
||||||
|
- Statut d'ancrage
|
||||||
|
- ID de transaction
|
||||||
|
- Hauteur de bloc (si disponible)
|
||||||
|
- Date d'ancrage
|
||||||
|
|
||||||
|
### Filigrane Visuel
|
||||||
|
|
||||||
|
**Spécifications** :
|
||||||
|
|
||||||
|
- **Texte** : "lecoffre.io"
|
||||||
|
- **Position** : Diagonal (45°), centré sur chaque page
|
||||||
|
- **Opacité** : 15% (visible mais non gênant)
|
||||||
|
- **Couleur** : Gris clair (#CCCCCC)
|
||||||
|
- **Police** : Arial, 48pt
|
||||||
|
- **Pages** : Toutes les pages du PDF
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vérification et Statuts
|
||||||
|
|
||||||
|
### Logique de Vérification
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT** : Le système d'ancrage V3 ne fonctionne plus avec des statuts intermédiaires. L'ancrage est maintenant **immédiat** :
|
||||||
|
|
||||||
|
- ✅ **Ancré** : L'ancrage a un `tx_id` (transaction ID Bitcoin)
|
||||||
|
- ❌ **Non ancré** : L'ancrage n'a pas de `tx_id`
|
||||||
|
|
||||||
|
**Il n'y a plus d'étape intermédiaire** comme `ATTEMPTING`, `QUEUED`, ou `VERIFYING_ON_CHAIN`.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
Le backend vérifie uniquement la présence de `tx_id` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (anchorStatus.tx_id) {
|
||||||
|
setAnchorStatus(AnchorStatus.VERIFIED_ON_CHAIN);
|
||||||
|
} else {
|
||||||
|
setAnchorStatus(AnchorStatus.NOT_ANCHORED);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
Le frontend utilise l'enum `AnchorStatus` mais ne vérifie que `tx_id` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export enum AnchorStatus {
|
||||||
|
"VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN",
|
||||||
|
"NOT_ANCHORED" = "NOT_ANCHORED",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification
|
||||||
|
if (anchorStatus.tx_id) {
|
||||||
|
setAnchorStatus(AnchorStatus.VERIFIED_ON_CHAIN);
|
||||||
|
} else {
|
||||||
|
setAnchorStatus(AnchorStatus.NOT_ANCHORED);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Masquage du Statut "Ancrage en cours"
|
||||||
|
|
||||||
|
**Objectif** : Améliorer l'expérience utilisateur en ne montrant pas le statut transitoire "Ancrage en cours" pour les documents validés.
|
||||||
|
|
||||||
|
**Comportement** : Pour les documents avec `document_status === VALIDATED`, le statut "Ancrage en cours" (`ATTEMPTING`) n'est **pas affiché** dans l'interface utilisateur.
|
||||||
|
|
||||||
|
**Composant `AnchorBadge`** :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (documentStatus === "VALIDATED" && status === "ATTEMPTING") {
|
||||||
|
return null; // Badge not displayed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification Publique
|
||||||
|
|
||||||
|
**Endpoints** :
|
||||||
|
|
||||||
|
- `GET /api/v1/public/verify/:hash` : Vérification par hash (SANS authentification)
|
||||||
|
- `POST /api/v1/public/verify-file` : Vérification par upload fichier (SANS authentification)
|
||||||
|
|
||||||
|
**Page frontend** : `/verify-document`
|
||||||
|
|
||||||
|
**Fonctionnalités** :
|
||||||
|
|
||||||
|
- Upload fichier (drag & drop ou sélection)
|
||||||
|
- Calcul hash automatique (SHA256)
|
||||||
|
- Appel API public (aucune authentification requise)
|
||||||
|
- Affichage résultat (success/error)
|
||||||
|
- Lien explorateur blockchain
|
||||||
|
|
||||||
|
### Procédure de Vérification
|
||||||
|
|
||||||
|
**Pour utilisateur final** :
|
||||||
|
|
||||||
|
1. Télécharger ZIP complet du dossier
|
||||||
|
2. Extraire le ZIP
|
||||||
|
3. Ouvrir fichier dans `04_preuves/preuve_XXX.json`
|
||||||
|
4. Noter le `watermarked_hash`
|
||||||
|
5. Calculer hash du fichier dans `02_filigranes/` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shasum -a 256 02_filigranes/acte_vente.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Comparer les deux hashs (doivent être identiques)
|
||||||
|
7. Vérifier TX : Ouvrir `tx_link` du JSON dans navigateur
|
||||||
|
8. ✅ **Validé** si hash OK + TX confirmée sur blockchain
|
||||||
|
|
||||||
|
**Pour audit technique** :
|
||||||
|
|
||||||
|
1. Parser JSON de preuve
|
||||||
|
2. Extraire `watermarked_hash`, `txid`, `block_height`
|
||||||
|
3. Requête API blockchain (Mempool Space) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://mempool.4nkweb.com/signet/api/tx/{txid}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Vérifier :
|
||||||
|
- TX existe
|
||||||
|
- Hash dans OP_RETURN correspond
|
||||||
|
- Block confirmé
|
||||||
|
5. Recalculer hash fichier filigrané
|
||||||
|
6. Comparer avec `watermarked_hash`
|
||||||
|
7. ✅ **Preuve authentique** si tout correspond
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Scripts et Maintenance
|
||||||
|
|
||||||
|
### Scripts Disponibles
|
||||||
|
|
||||||
|
#### Audit des Ancrages Manquants
|
||||||
|
|
||||||
|
**Script** : `audit-missing-anchors.js`
|
||||||
|
|
||||||
|
**Usage** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=prod npm run anchorage:audit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
|
||||||
|
- Analyse tous les documents validés sans ancrage
|
||||||
|
- Analyse tous les dossiers archivés sans ancrage
|
||||||
|
- Génère un CSV avec la liste des documents/dossiers à réancrer
|
||||||
|
- Ne modifie aucune donnée (lecture seule)
|
||||||
|
|
||||||
|
**Résultat** :
|
||||||
|
|
||||||
|
- Affichage console avec statistiques
|
||||||
|
- CSV généré : `logs/audit-missing-anchors_YYYYMMDD_HHMMSS.csv`
|
||||||
|
|
||||||
|
#### Ancrage V3 de Dossiers Existants
|
||||||
|
|
||||||
|
**Script** : `anchor-existing-folders-v3.js`
|
||||||
|
|
||||||
|
**Usage** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ancrage tous dossiers LIVE + ARCHIVED (par défaut)
|
||||||
|
ENV=prod npm run anchorage
|
||||||
|
|
||||||
|
# Dry-run (simulation)
|
||||||
|
ENV=prod npm run anchorage -- --all --batch=1 --dry-run
|
||||||
|
|
||||||
|
# Ancrage réel avec limite batch
|
||||||
|
ENV=prod npm run anchorage -- --all --batch=5
|
||||||
|
|
||||||
|
# Ancrage de dossiers spécifiques
|
||||||
|
ENV=prod npm run anchorage -- --folder-uids=uid1,uid2,uid3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options** :
|
||||||
|
|
||||||
|
- `--all-archived` : Tous les dossiers ARCHIVED sans ancrage
|
||||||
|
- `--all-validated` : Tous les dossiers VALIDATED sans ancrage
|
||||||
|
- `--all` : Tous les dossiers VALIDATED + ARCHIVED
|
||||||
|
- `--folder-uids=uid1,uid2,...` : Liste UIDs dossiers spécifiques
|
||||||
|
- `--batch=N` : Nombre max de dossiers par batch (défaut: 10)
|
||||||
|
- `--dry-run` : Simulation sans ancrage réel
|
||||||
|
- `--skip-documents` : Ancrer seulement dossiers (pas documents individuels)
|
||||||
|
|
||||||
|
#### Vérifications Systématisées des Colonnes d'Ancrage
|
||||||
|
|
||||||
|
Depuis 2025-01-XX, les scripts de déploiement vérifient systématiquement la présence des colonnes indispensables pour l'ancrage avant d'exécuter le réancrage.
|
||||||
|
|
||||||
|
**Colonnes vérifiées** :
|
||||||
|
|
||||||
|
- `document_anchors.proof_data` (JSONB) - Données de preuve structurées
|
||||||
|
- `document_anchors.confirmations` (INTEGER) - Nombre de confirmations blockchain
|
||||||
|
- `document_anchors.anchor_job_id` (VARCHAR) - ID du job d'ancrage
|
||||||
|
- `document_notary_anchors.proof_data` (JSONB) - Données de preuve structurées
|
||||||
|
- `document_notary_anchors.confirmations` (INTEGER) - Nombre de confirmations blockchain
|
||||||
|
|
||||||
|
**Garanties** :
|
||||||
|
|
||||||
|
- ✅ Vérification de l'existence des tables avant toute modification
|
||||||
|
- ✅ Ajout automatique des colonnes manquantes (8 points de vérification)
|
||||||
|
- ✅ Blocage du réancrage si colonnes manquantes détectées
|
||||||
|
- ✅ Messages d'erreur explicites indiquant les colonnes manquantes
|
||||||
|
|
||||||
|
**Ordre d'exécution garanti** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
resetDatabase → migrateResolveDatabase → reanchorAll
|
||||||
|
↓ ↓ ↓
|
||||||
|
Base vide Colonnes ajoutées Vérification finale
|
||||||
|
↓ ↓
|
||||||
|
Si manquantes → Blocage si manquantes
|
||||||
|
```
|
||||||
|
|
||||||
|
📖 Voir [README.md](./README.md#consolidation-operationnelle-ex-operationsmd) et [DEPLOYMENT.md](./DEPLOYMENT.md) pour les corrections sur les colonnes d'ancrage.
|
||||||
|
|
||||||
|
#### Réancrage de Documents/Dossiers
|
||||||
|
|
||||||
|
**Script** : `reanchor-documents.js`
|
||||||
|
|
||||||
|
**Usage** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry-run (simulation)
|
||||||
|
ENV=prod npm run anchorage:reanchor -- --dry-run
|
||||||
|
|
||||||
|
# Réancrage réel
|
||||||
|
ENV=prod npm run anchorage:reanchor -- --document-uids=uid1,uid2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options** :
|
||||||
|
|
||||||
|
- `--document-uids=uid1,uid2,...` : Liste UIDs documents spécifiques
|
||||||
|
- `--folder-uids=uid1,uid2,...` : Liste UIDs dossiers spécifiques
|
||||||
|
- `--all-missing` : Réancrer TOUS les documents/dossiers sans ancrage (⚠️)
|
||||||
|
- `--batch=N` : Nombre max de documents/dossiers par batch (défaut: 50)
|
||||||
|
- `--dry-run` : Simulation sans ancrage réel
|
||||||
|
|
||||||
|
#### Nettoyage Ancres Dossiers Incomplets
|
||||||
|
|
||||||
|
**Script** : `clean-incomplete-folder-anchors.ts`
|
||||||
|
|
||||||
|
**Contexte** :
|
||||||
|
|
||||||
|
- Certains dossiers peuvent avoir été ancrés alors qu'ils n'étaient pas à 100%
|
||||||
|
- Seuls les dossiers complets (100% documents validés) doivent être ancrés
|
||||||
|
|
||||||
|
**Usage** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis le conteneur backend
|
||||||
|
cd lecoffre-back-main
|
||||||
|
npx ts-node src/scripts/clean-incomplete-folder-anchors.ts --dry-run
|
||||||
|
npx ts-node src/scripts/clean-incomplete-folder-anchors.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions** :
|
||||||
|
|
||||||
|
- Identifie les dossiers ancrés avec complétion < 100%
|
||||||
|
- Supprime les ancres correspondantes dans `office_folder_anchors`
|
||||||
|
- Réinitialise les liens `folder_anchor_uid = NULL` dans `office_folders`
|
||||||
|
|
||||||
|
### ⚠️ IMPORTANT : Prérequis
|
||||||
|
|
||||||
|
1. **Colonnes V3** : La base doit avoir toutes les colonnes V3 (`watermarked_s3_key`, `watermarked_at`, `zip_hash`, `zip_s3_key`, `zip_size`, `proof_data`)
|
||||||
|
2. **Prisma Client** : Le client Prisma doit être généré avec le bon schéma
|
||||||
|
3. **Connexion BDD** : Les scripts utilisent `DATABASE_URL` (configurée sur le serveur ou via `.env.<env>`)
|
||||||
|
|
||||||
|
### Workflow Recommandé
|
||||||
|
|
||||||
|
1. **Audit initial** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=prod npm run anchorage:audit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test avec dry-run** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=prod npm run anchorage -- --all-archived --batch=1 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Ancrage réel** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENV=prod npm run anchorage -- --all-archived --batch=5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Troubleshooting
|
||||||
|
|
||||||
|
### Certificats Manquants dans le ZIP
|
||||||
|
|
||||||
|
**Symptôme** : Lors de la validation d'un document, le document apparaît comme validé, mais le certificat d'ancrage n'est pas présent dans le ZIP téléchargé.
|
||||||
|
|
||||||
|
**Root Cause** :
|
||||||
|
|
||||||
|
1. Recherche par hash uniquement (peut échouer)
|
||||||
|
2. Pas de vérification du statut avant génération
|
||||||
|
|
||||||
|
**Solution** :
|
||||||
|
|
||||||
|
1. Recherche prioritaire par `document_uid` (plus fiable)
|
||||||
|
2. Fallback par hash si nécessaire
|
||||||
|
3. Vérification du statut (`status === VERIFIED_ON_CHAIN`) avant génération
|
||||||
|
|
||||||
|
### Timing et Synchronisation
|
||||||
|
|
||||||
|
**Scénario typique** :
|
||||||
|
|
||||||
|
1. **T0** : Document validé → Ancrage lancé en arrière-plan
|
||||||
|
2. **T0+2s** : Ancrage créé avec `status = ATTEMPTING`
|
||||||
|
3. **T0+5s** : Transaction Bitcoin confirmée → `status = VERIFIED_ON_CHAIN` (après 6 confirmations)
|
||||||
|
4. **T0+10s** : Utilisateur télécharge le ZIP → Certificat inclus ✅
|
||||||
|
|
||||||
|
**Cas limite** : Si l'utilisateur télécharge le ZIP **immédiatement après validation** (avant que l'ancrage soit vérifié) :
|
||||||
|
|
||||||
|
- ⚠️ L'ancrage peut être en statut `ATTEMPTING` ou `QUEUED`
|
||||||
|
- ⚠️ Le certificat **ne sera pas inclus** dans le ZIP
|
||||||
|
- ✅ L'utilisateur peut **relancer le téléchargement** plus tard pour obtenir le certificat
|
||||||
|
|
||||||
|
### Fichiers Sans Clé de Chiffrement (.expired)
|
||||||
|
|
||||||
|
**Contexte** : Les dumps de base de données provenant de versions antérieures peuvent contenir des fichiers avec `key = null` (clé de chiffrement perdue).
|
||||||
|
|
||||||
|
**Solution implémentée** :
|
||||||
|
|
||||||
|
- `FilesService.download()` : Retourne fichier vide `.expired` si `key = null`
|
||||||
|
- `WatermarkService.addWatermarkAndUpload()` : Upload fichier vide `.expired` sur IPFS
|
||||||
|
- `DocumentAnchorsService.anchorDocument()` : Ancrage avec hash du buffer vide
|
||||||
|
|
||||||
|
**Résultat** :
|
||||||
|
|
||||||
|
- ✅ Scripts d'ancrage ne crashent plus sur fichiers sans clé
|
||||||
|
- ✅ Fichiers `.expired` créés automatiquement
|
||||||
|
- ✅ Ancrage blockchain avec hash du buffer vide
|
||||||
|
- ⚠️ Utilisateur informé que fichier non récupérable
|
||||||
|
|
||||||
|
### Checklist de Vérification
|
||||||
|
|
||||||
|
Pour diagnostiquer un problème de certificat manquant dans un ZIP :
|
||||||
|
|
||||||
|
- [ ] Vérifier que le document est **validé** (`document_status === VALIDATED`)
|
||||||
|
- [ ] Vérifier qu'un ancrage existe pour ce document (`document_anchors` table)
|
||||||
|
- [ ] Vérifier le **statut de l'ancrage** (`status === VERIFIED_ON_CHAIN`)
|
||||||
|
- [ ] Vérifier les **logs backend** pour voir si l'ancrage a été trouvé
|
||||||
|
- [ ] Vérifier que `includeAnchors = true` lors de l'appel à `createZipFromFiles`
|
||||||
|
- [ ] Vérifier que le fichier n'est pas un `isNotaryFiles` (pas d'ancrage pour les fichiers notaire)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Références
|
||||||
|
|
||||||
|
### Documentation Détaillée
|
||||||
|
|
||||||
|
- **Architecture V3** : `docs/ANCRAGE_V3_ARCHITECTURE.md` - Architecture complète V3
|
||||||
|
- **Certificats et ZIP** : `docs/ANCRAGE_CERTIFICATS_ZIP.md` - Génération certificats et ZIP
|
||||||
|
- **Scripts** : `docs/SCRIPTS.md` - Scripts d'audit et d'ancrage
|
||||||
|
- **Statuts** : `docs/ANCHOR_STATUS_VERIFICATION.md` - Vérification des statuts d'ancrage
|
||||||
|
- **Séquences de traitement** : `docs/DOCUMENT_PROCESSING_SEQUENCES_V2.md` - Séquence complète upload/ancrage
|
||||||
|
|
||||||
|
### Services Backend
|
||||||
|
|
||||||
|
- **WatermarkService** : `lecoffre-back-main/src/services/common/WatermarkService/WatermarkService.ts`
|
||||||
|
- **DocumentAnchorsService** : `lecoffre-back-main/src/services/notary/DocumentAnchorsService/DocumentAnchorsService.ts`
|
||||||
|
- **OfficeFolderAnchorsService** : `lecoffre-back-main/src/services/notary/OfficeFolderAnchorsService/OfficeFolderAnchorsService.ts`
|
||||||
|
- **ProofDataService** : `lecoffre-back-main/src/services/notary/ProofDataService/ProofDataService.ts`
|
||||||
|
- **BitcoinSignetService** : `lecoffre-back-main/src/services/common/BitcoinSignetService/BitcoinSignetService.ts`
|
||||||
|
- **AnchorCertificateService** : `lecoffre-back-main/src/services/notary/AnchorCertificateService/AnchorCertificateService.ts`
|
||||||
|
- **ZipService** : `lecoffre-back-main/src/services/common/ZipService/ZipService.ts`
|
||||||
|
|
||||||
|
### Controllers
|
||||||
|
|
||||||
|
- **DocumentsController** : `lecoffre-back-main/src/app/api/notary/DocumentsController.ts`
|
||||||
|
- **FilesController (Notary)** : `lecoffre-back-main/src/app/api/notary/FilesController.ts`
|
||||||
|
- **FilesController (Customer)** : `lecoffre-back-main/src/app/api/customer/FilesController.ts`
|
||||||
|
- **PublicAnchorVerificationController** : `lecoffre-back-main/src/app/api/public/PublicAnchorVerificationController.ts`
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
- **Audit** : `lecoffre-back-main/src/scripts/audit-missing-anchors.ts`
|
||||||
|
- **Ancrage dossiers** : `lecoffre-back-main/src/scripts/anchor-existing-folders-v3.ts`
|
||||||
|
- **Réancrage** : `lecoffre-back-main/src/scripts/reanchor-documents.ts`
|
||||||
|
- **Nettoyage** : `lecoffre-back-main/src/scripts/clean-incomplete-folder-anchors.ts`
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Page vérification** : `lecoffre-front-main/src/pages/verify-document.tsx`
|
||||||
|
- **Composant AnchorBadge** : `lecoffre-front-main/src/front/Components/DesignSystem/AnchorBadge/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 2025-11-24
|
||||||
|
**Version** : 3.1.2
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user