From 2569b3f0bae397a1474a59961113eb2bff282505 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 02:27:05 -0400 Subject: [PATCH 1/9] [FIX] todo.py remove tk, not used - this broke server without gui for pyton 3.12.3 --- script/todo/todo.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 16e758a..88b2102 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -41,9 +41,6 @@ GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle") try: - import tkinter as tk - from tkinter import filedialog - import click import dotenv import humanize @@ -1385,9 +1382,6 @@ class TODO: # # self.restart_script(e) try: # TODO duplicate - import tkinter as tk - from tkinter import filedialog - import click import humanize import openai From ac30054d0703592d621bfad9b77869d210cb8712 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 02:50:18 -0400 Subject: [PATCH 2/9] [REF] claude: split CLAUDE.md into .claude/rules/ Keep CLAUDE.md lean (~44 lines) with project identity, core principles, and Claude-specific instructions only. Move detailed documentation into 9 thematic rule files under .claude/rules/ for better context optimization. Generated by Claude Code 2.1.74 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit --- .claude/rules/01-versions.md | 14 ++ .claude/rules/02-project-structure.md | 45 ++++ .claude/rules/03-commands.md | 63 +++++ .claude/rules/04-code-conventions.md | 24 ++ .claude/rules/05-environments.md | 22 ++ .claude/rules/06-code-generator.md | 9 + .claude/rules/07-documentation.md | 55 +++++ .claude/rules/08-deployment.md | 9 + .claude/rules/09-workflow.md | 49 ++++ CLAUDE.md | 316 ++------------------------ 10 files changed, 306 insertions(+), 300 deletions(-) create mode 100644 .claude/rules/01-versions.md create mode 100644 .claude/rules/02-project-structure.md create mode 100644 .claude/rules/03-commands.md create mode 100644 .claude/rules/04-code-conventions.md create mode 100644 .claude/rules/05-environments.md create mode 100644 .claude/rules/06-code-generator.md create mode 100644 .claude/rules/07-documentation.md create mode 100644 .claude/rules/08-deployment.md create mode 100644 .claude/rules/09-workflow.md diff --git a/.claude/rules/01-versions.md b/.claude/rules/01-versions.md new file mode 100644 index 0000000..5201eed --- /dev/null +++ b/.claude/rules/01-versions.md @@ -0,0 +1,14 @@ +# Versions supportées + +| Odoo | Python | Poetry | Statut | +|-------|----------|--------|------------| +| 18.0 | 3.12.10 | 2.1.3 | **Défaut** | +| 17.0 | 3.10.18 | 1.8.3 | Actif | +| 16.0 | 3.10.18 | 1.8.3 | Actif | +| 15.0 | 3.8.20 | 1.8.3 | Déprécié | +| 14.0 | 3.8.20 | 1.5.0 | Déprécié | +| 13.0 | 3.7.17 | 1.5.0 | Déprécié | +| 12.0 | 3.7.17 | 1.5.0 | Déprécié | + +Configuration dans `conf/supported_version_erplibre.json`. +Fichiers de version : `.odoo-version`, `.erplibre-version`, `.poetry-version`, `.python-odoo-version`. diff --git a/.claude/rules/02-project-structure.md b/.claude/rules/02-project-structure.md new file mode 100644 index 0000000..dffc4c0 --- /dev/null +++ b/.claude/rules/02-project-structure.md @@ -0,0 +1,45 @@ +# Structure du projet + +``` +erplibre/ +├── Makefile # Orchestrateur principal (inclut conf/make.*.Makefile) +├── run.sh / odoo_bin.sh # Lanceurs Odoo (venv + PYTHONPATH) +├── env_var.sh # Variables d'environnement globales +├── conf/ # Configuration : Makefiles modulaires, versions, manifests CSV +│ ├── make.installation.Makefile +│ ├── make.test.Makefile +│ ├── make.database.Makefile +│ ├── make.docker.Makefile +│ ├── make.code_generator.Makefile +│ ├── make.installation.poetry.Makefile +│ └── supported_version_erplibre.json +├── manifest/ # Manifests Google Repo (XML) par version Odoo +│ └── git_manifest_odoo{12..18}.0.xml +├── requirement/ # Dépendances par version +│ ├── pyproject.odooXX.0_pythonY.Z.toml +│ ├── poetry.odooXX.0_pythonY.Z.lock +│ └── requirements.odooXX.0_pythonY.Z.txt +├── script/ # Scripts utilitaires (32+ catégories) +│ ├── todo/ # CLI interactif principal (todo.py) +│ ├── database/ # Opérations DB (restore, migrate, image_db) +│ ├── addons/ # Gestion des modules (install, update, uninstall) +│ ├── code_generator/ # Génération de modules Odoo +│ ├── version/ # Changement de version +│ ├── git/ # Opérations Git et Google Repo +│ ├── maintenance/ # Formatage (black, isort, prettier) +│ ├── test/ # Tests parallèles + coverage +│ ├── docker/ # Build/run Docker +│ ├── poetry/ # Gestion Poetry +│ ├── deployment/ # Déploiement production +│ └── selenium/ # Tests web automatisés +├── docker/ # Dockerfiles + docker-compose par version +├── addons/ # Répertoire des addons (géré par Google Repo) +│ ├── OCA_*/ # Modules OCA +│ ├── ERPLibre_*/ # Modules ERPLibre +│ ├── TechnoLibre_*/ # Code generator + templates +│ └── MathBenTech_*/ # Modules spécialisés +├── odoo{12..18}.0/ # Sources Odoo par version +├── doc/ # Documentation (DEVELOPMENT, PRODUCTION, MIGRATION, etc.) +├── test/ # Framework de test +└── private/ # Fichiers privés (non versionné) +``` diff --git a/.claude/rules/03-commands.md b/.claude/rules/03-commands.md new file mode 100644 index 0000000..06dd2ae --- /dev/null +++ b/.claude/rules/03-commands.md @@ -0,0 +1,63 @@ +# Commandes essentielles + +## Changement de version Odoo +```bash +make switch_odoo_18 # Passer à Odoo 18 +make switch_odoo_17 # Passer à Odoo 17 +make switch_odoo_16 # Passer à Odoo 16 (défaut) +make switch_odoo_15 # Passer à Odoo 15 +make switch_odoo_14 # Passer à Odoo 14 +make switch_odoo_13 # Passer à Odoo 13 +make switch_odoo_12 # Passer à Odoo 12 +make switch_odoo_16_update # Passer + mettre à jour les dépendances +``` + +## Exécution +```bash +make run # Lancer Odoo (port 8069) +make run_test # Lancer avec la DB "test" +./run.sh -d ma_base # Lancer avec une DB spécifique +./odoo_bin.sh [args] # Exécuter odoo-bin directement +``` + +## Base de données +```bash +make db_create_db_test # Créer la DB "test" +make db_clone_test_to_test2 # Cloner test -> test2 +./script/database/db_restore.py --database NOM --image IMAGE +./script/addons/install_addons_dev.sh DB module1,module2 +./script/addons/update_addons_all.sh DB +``` + +## Tests +```bash +make test # Tests de base + format +make test_full_fast # Tests complets en parallèle +make test_full_fast_coverage # Avec couverture de code +``` + +## Formatage du code +```bash +make format # Fichiers à commiter uniquement +make format_all # Tout formater (parallèle) +``` + +## Docker +```bash +make docker_run_daemon # Lancer en production +make docker_build_odoo_18 # Builder l'image Odoo 18 +``` + +## Google Repo (gestion multi-dépôts) +```bash +make repo_show_status # Statut de tous les dépôts addons +make repo_configure_all # Configurer tous les dépôts +make repo_do_stash # Stash tous les dépôts +.venv.erplibre/bin/repo sync # Synchroniser les dépôts +``` + +## Installation +```bash +make install_os # Dépendances système +make install_odoo_18 # Installer Odoo 18 complet +``` diff --git a/.claude/rules/04-code-conventions.md b/.claude/rules/04-code-conventions.md new file mode 100644 index 0000000..7238f5d --- /dev/null +++ b/.claude/rules/04-code-conventions.md @@ -0,0 +1,24 @@ +# Conventions de code + +## Python +- Formateur : **Black** (profil par défaut, ligne max 79 pour les modules Odoo) +- Imports : **isort** avec profil `black`, longueur de ligne 79 +- Linting : **Flake8** avec bugbear, max-line-length 80, max-complexity 16 +- Ignorer : E203, E501, W503 (compatibilité Black) + +## XML / JSON / YAML +- Formateur : **Prettier** (via npm) +- Indentation : 4 espaces (XML/CSS/JS), 2 espaces (JSON/YAML) + +## Fichiers +- Encodage : UTF-8 +- Fins de ligne : LF (Unix) +- Indentation : 4 espaces (Python, XML, CSS, JS), 2 espaces (JSON, YAML, RST, MD) +- Retour à la ligne final : oui +- Espaces en fin de ligne : supprimés + +## Git +- Branches : `develop` (développement), `master` (production) +- Pas de submodules Git — utilise **Google Repo** pour les addons +- Manifests XML dans `manifest/` pour chaque version Odoo +- Format de commit : `[TYPE] description` (ex: `[FIX]`, `[UPD]`, `[ADD]`, `[REM]`) diff --git a/.claude/rules/05-environments.md b/.claude/rules/05-environments.md new file mode 100644 index 0000000..cb8998a --- /dev/null +++ b/.claude/rules/05-environments.md @@ -0,0 +1,22 @@ +# Architecture des environnements virtuels + +``` +.venv.erplibre/ # Venv ERPLibre (outils : repo, poetry, coverage) +.venv.odoo18/ # Venv Odoo 18 (Python 3.12) +.venv.odoo17/ # Venv Odoo 17 (Python 3.10) +.venv.odoo16/ # Venv Odoo 16 (Python 3.10) +.venv.odoo14/ # Venv Odoo 14 (Python 3.8) +.venv.odoo12/ # Venv Odoo 12 (Python 3.7) +``` + +Géré via **pyenv** pour les multiples versions de Python. + +## Système de dépendances + +Chaque version Odoo a son propre ensemble dans `requirement/` : +- `pyproject.odooXX.0_pythonY.Z.toml` — Configuration Poetry +- `poetry.odooXX.0_pythonY.Z.lock` — Lock file Poetry +- `requirements.odooXX.0_pythonY.Z.txt` — Requirements pip (fallback) +- `ignore_requirements.odooXX.0.txt` — Paquets à ignorer + +Mise à jour : `./script/poetry/poetry_update.py` diff --git a/.claude/rules/06-code-generator.md b/.claude/rules/06-code-generator.md new file mode 100644 index 0000000..51ca59c --- /dev/null +++ b/.claude/rules/06-code-generator.md @@ -0,0 +1,9 @@ +# Code Generator + +ERPLibre inclut un système de génération de modules Odoo : +- `script/code_generator/new_project.py` — Créer un nouveau module +- `script/code_generator/create_from_existing_module.py` — Cloner un module existant +- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération +- `addons/TechnoLibre_odoo-code-generator-template/` — Templates + +Documentation : `doc/CODE_GENERATOR.md` diff --git a/.claude/rules/07-documentation.md b/.claude/rules/07-documentation.md new file mode 100644 index 0000000..ded55b5 --- /dev/null +++ b/.claude/rules/07-documentation.md @@ -0,0 +1,55 @@ +# Documentation multilingue + +La documentation est bilingue (anglais/français) via **mmg** (Multilingual Markdown Generator). + +## Fonctionnement +- Les fichiers sources sont les `.base.md` (contiennent les deux langues) +- `mmg` génère : `FICHIER.md` (anglais) et `FICHIER.fr.md` (français) +- Marqueurs : ``, ``, `` (blocs de code partagés) + +## Commandes +```bash +make doc_markdown # Regénérer toute la doc multilingue +``` + +## Convention +- **Ne jamais modifier directement** les fichiers `.md` ou `.fr.md` générés +- Toujours modifier le fichier `.base.md` correspondant, puis exécuter `make doc_markdown` +- Les blocs de code vont dans ``, le texte dans `` et `` +- En-tête obligatoire dans chaque `.base.md` : +``` + + + + +``` + +## Fichiers concernés (30 fichiers) +- Racine : `README`, `CHANGELOG`, `TODO` +- `doc/` : DEVELOPMENT, PRODUCTION, DISCOVER, RUN, MIGRATION, WINDOWS_INSTALLATION, FAQ, GIT_REPO, POETRY, RELEASE, UPDATE, CONTRIBUTION, HOWTO, TODO, CODE_GENERATOR +- `docker/` : README +- `script/*/` : database, deployment, fork_github_repo, nginx, restful, selenium (2), todo, odoo/migration +- `.github/ISSUE_TEMPLATE/` : bug_report, feature_request + +## Internationalisation du CLI TODO (i18n) + +Le CLI interactif `script/todo/todo.py` supporte le français et l'anglais. + +### Architecture +- **`script/todo/todo_i18n.py`** — Module de traduction (dictionnaire `TRANSLATIONS`, fonctions `t()`, `get_lang()`, `set_lang()`) +- Les chaînes traduisibles utilisent `t("clé")` au lieu de texte en dur +- Les entrées de `todo.json` peuvent avoir un champ `prompt_description_key` résolu via `t()` (fallback sur `prompt_description`) + +### Résolution de la langue (priorité) +1. Variable d'environnement `EL_LANG` (définie dans `env_var.sh`, défaut `"fr"`) +2. Défaut : `"fr"` + +### Comportement +- Première exécution : prompt bilingue demande à l'utilisateur de choisir sa langue +- Le choix est persisté dans `env_var.sh` +- Changement de langue possible via le menu Execute > Langue/Language + +### Ajouter une traduction +1. Ajouter la clé dans `TRANSLATIONS` de `todo_i18n.py` avec les valeurs `"fr"` et `"en"` +2. Remplacer la chaîne en dur par `t("ma_clé")` dans `todo.py` +3. Pour les entrées JSON : ajouter `"prompt_description_key": "ma_clé"` dans `todo.json` diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md new file mode 100644 index 0000000..d7f7935 --- /dev/null +++ b/.claude/rules/08-deployment.md @@ -0,0 +1,9 @@ +# Déploiement + +- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6) +- **Systemd** : `script/systemd/` pour les services +- **Nginx** : `script/nginx/` pour le reverse proxy +- **SSL** : Certbot pour les certificats +- **DNS** : `script/deployment/update_dns_cloudflare.py` + +Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). diff --git a/.claude/rules/09-workflow.md b/.claude/rules/09-workflow.md new file mode 100644 index 0000000..aa5f335 --- /dev/null +++ b/.claude/rules/09-workflow.md @@ -0,0 +1,49 @@ +# Workflow Orchestration + +## 1. Plan Mode Default +- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) +- If something goes sideways, STOP and re-plan immediately +- Don't keep pushing. +- Use plan mode for verification steps, not just building +- Write detailed specs upfront to reduce ambiguity + +## 2. Subagent Strategy +- Use subagents liberally to keep the main context window clean. +- Offload research, exploration, and parallel analysis to subagents +- For complex problems, throw more compute at it via subagents +- One task per subagent for focused execution + +## 3. Self-Improvement Loop +- Capture Lessons: Update AGENT.md with any change in approach the user has asked you to make. +- Write rules for yourself that prevent the same mistake +- Ruthlessly iterate on these lessons until the mistake rate drops. +- Review lessons at session start for relevant project + +## 4. Verification Before Done +- Never mark a task complete without proving it works +- Diff behavior between main and your changes when relevant +- Ask yourself, "Would a staff engineer approve this?" +- Run tests, check logs, and demonstrate correctness. + +## 5. Demand Elegance (Balanced) +- For non-trivial changes, pause and ask, "Is there a more elegant way?" +- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution" +- Skip this for simple, obvious fixes +- Don't over-engineer. +- Challenge your own work before presenting it + +## 6. Autonomous Bug Fixing +- When given a bug report, just fix it. Don't ask for hand-holding +- Point at logs, errors, and failing tests. +- Then resolve them. +- Zero context switching required from the user +- Go fix failing CI tests without being told how + +## Task Management + +1. **Plan First**: Write a plan to `tasks/todo.md` with checkable items. +2. **Verify Plan**: Check in before starting implementation. +3. **Track Progress**: Mark items complete as you go. +4. **Explain Changes**: High-level summary at each step. +5. **Document Results**: Add review section to `tasks/todo.md` +6. **Capture Lessons**: Update `tasks/lessons.md` after corrections diff --git a/CLAUDE.md b/CLAUDE.md index b98adc8..5e5fc65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,256 +6,6 @@ ERPLibre est un fork communautaire d'Odoo Community Edition (OCE) supportant les Version actuelle : **1.6.0** | Licence : **AGPL-3.0+** Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.6.0) -## Versions supportées - -| Odoo | Python | Poetry | Statut | -|-------|----------|--------|------------| -| 18.0 | 3.12.10 | 2.1.3 | **Défaut** | -| 17.0 | 3.10.18 | 1.8.3 | Actif | -| 16.0 | 3.10.18 | 1.8.3 | Actif | -| 15.0 | 3.8.20 | 1.8.3 | Déprécié | -| 14.0 | 3.8.20 | 1.5.0 | Déprécié | -| 13.0 | 3.7.17 | 1.5.0 | Déprécié | -| 12.0 | 3.7.17 | 1.5.0 | Déprécié | - -Configuration dans `conf/supported_version_erplibre.json`. -Fichiers de version : `.odoo-version`, `.erplibre-version`, `.poetry-version`, `.python-odoo-version`. - -## Structure du projet - -``` -erplibre/ -├── Makefile # Orchestrateur principal (inclut conf/make.*.Makefile) -├── run.sh / odoo_bin.sh # Lanceurs Odoo (venv + PYTHONPATH) -├── env_var.sh # Variables d'environnement globales -├── conf/ # Configuration : Makefiles modulaires, versions, manifests CSV -│ ├── make.installation.Makefile -│ ├── make.test.Makefile -│ ├── make.database.Makefile -│ ├── make.docker.Makefile -│ ├── make.code_generator.Makefile -│ ├── make.installation.poetry.Makefile -│ └── supported_version_erplibre.json -├── manifest/ # Manifests Google Repo (XML) par version Odoo -│ └── git_manifest_odoo{12..18}.0.xml -├── requirement/ # Dépendances par version -│ ├── pyproject.odooXX.0_pythonY.Z.toml -│ ├── poetry.odooXX.0_pythonY.Z.lock -│ └── requirements.odooXX.0_pythonY.Z.txt -├── script/ # Scripts utilitaires (32+ catégories) -│ ├── todo/ # CLI interactif principal (todo.py) -│ ├── database/ # Opérations DB (restore, migrate, image_db) -│ ├── addons/ # Gestion des modules (install, update, uninstall) -│ ├── code_generator/ # Génération de modules Odoo -│ ├── version/ # Changement de version -│ ├── git/ # Opérations Git et Google Repo -│ ├── maintenance/ # Formatage (black, isort, prettier) -│ ├── test/ # Tests parallèles + coverage -│ ├── docker/ # Build/run Docker -│ ├── poetry/ # Gestion Poetry -│ ├── deployment/ # Déploiement production -│ └── selenium/ # Tests web automatisés -├── docker/ # Dockerfiles + docker-compose par version -├── addons/ # Répertoire des addons (géré par Google Repo) -│ ├── OCA_*/ # Modules OCA -│ ├── ERPLibre_*/ # Modules ERPLibre -│ ├── TechnoLibre_*/ # Code generator + templates -│ └── MathBenTech_*/ # Modules spécialisés -├── odoo{12..18}.0/ # Sources Odoo par version -├── doc/ # Documentation (DEVELOPMENT, PRODUCTION, MIGRATION, etc.) -├── test/ # Framework de test -└── private/ # Fichiers privés (non versionné) -``` - -## Commandes essentielles - -### Changement de version Odoo -```bash -make switch_odoo_18 # Passer à Odoo 18 -make switch_odoo_17 # Passer à Odoo 17 -make switch_odoo_16 # Passer à Odoo 16 (défaut) -make switch_odoo_15 # Passer à Odoo 15 -make switch_odoo_14 # Passer à Odoo 14 -make switch_odoo_13 # Passer à Odoo 13 -make switch_odoo_12 # Passer à Odoo 12 -make switch_odoo_16_update # Passer + mettre à jour les dépendances -``` - -### Exécution -```bash -make run # Lancer Odoo (port 8069) -make run_test # Lancer avec la DB "test" -./run.sh -d ma_base # Lancer avec une DB spécifique -./odoo_bin.sh [args] # Exécuter odoo-bin directement -``` - -### Base de données -```bash -make db_create_db_test # Créer la DB "test" -make db_clone_test_to_test2 # Cloner test -> test2 -./script/database/db_restore.py --database NOM --image IMAGE -./script/addons/install_addons_dev.sh DB module1,module2 -./script/addons/update_addons_all.sh DB -``` - -### Tests -```bash -make test # Tests de base + format -make test_full_fast # Tests complets en parallèle -make test_full_fast_coverage # Avec couverture de code -``` - -### Formatage du code -```bash -make format # Fichiers à commiter uniquement -make format_all # Tout formater (parallèle) -``` - -Outils : **Black** (Python, ligne max 79), **isort** (profil black, ligne max 79), **Prettier** (XML/JSON), **Flake8** (ligne max 80, complexité max 16). - -### Docker -```bash -make docker_run_daemon # Lancer en production -make docker_build_odoo_18 # Builder l'image Odoo 18 -``` - -### Google Repo (gestion multi-dépôts) -```bash -make repo_show_status # Statut de tous les dépôts addons -make repo_configure_all # Configurer tous les dépôts -make repo_do_stash # Stash tous les dépôts -.venv.erplibre/bin/repo sync # Synchroniser les dépôts -``` - -### Installation -```bash -make install_os # Dépendances système -make install_odoo_18 # Installer Odoo 18 complet -``` - -## Conventions de code - -### Python -- Formateur : **Black** (profil par défaut, ligne max 79 pour les modules Odoo) -- Imports : **isort** avec profil `black`, longueur de ligne 79 -- Linting : **Flake8** avec bugbear, max-line-length 80, max-complexity 16 -- Ignorer : E203, E501, W503 (compatibilité Black) - -### XML / JSON / YAML -- Formateur : **Prettier** (via npm) -- Indentation : 4 espaces (XML/CSS/JS), 2 espaces (JSON/YAML) - -### Fichiers -- Encodage : UTF-8 -- Fins de ligne : LF (Unix) -- Indentation : 4 espaces (Python, XML, CSS, JS), 2 espaces (JSON, YAML, RST, MD) -- Retour à la ligne final : oui -- Espaces en fin de ligne : supprimés - -### Git -- Branches : `develop` (développement), `master` (production) -- Pas de submodules Git — utilise **Google Repo** pour les addons -- Manifests XML dans `manifest/` pour chaque version Odoo - -## Architecture des environnements virtuels - -``` -.venv.erplibre/ # Venv ERPLibre (outils : repo, poetry, coverage) -.venv.odoo18/ # Venv Odoo 18 (Python 3.12) -.venv.odoo17/ # Venv Odoo 17 (Python 3.10) -.venv.odoo16/ # Venv Odoo 16 (Python 3.10) -.venv.odoo14/ # Venv Odoo 14 (Python 3.8) -.venv.odoo12/ # Venv Odoo 12 (Python 3.7) -``` - -Géré via **pyenv** pour les multiples versions de Python. - -## Système de dépendances - -Chaque version Odoo a son propre ensemble dans `requirement/` : -- `pyproject.odooXX.0_pythonY.Z.toml` — Configuration Poetry -- `poetry.odooXX.0_pythonY.Z.lock` — Lock file Poetry -- `requirements.odooXX.0_pythonY.Z.txt` — Requirements pip (fallback) -- `ignore_requirements.odooXX.0.txt` — Paquets à ignorer - -Mise à jour : `./script/poetry/poetry_update.py` - -## Code Generator - -ERPLibre inclut un système de génération de modules Odoo : -- `script/code_generator/new_project.py` — Créer un nouveau module -- `script/code_generator/create_from_existing_module.py` — Cloner un module existant -- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération -- `addons/TechnoLibre_odoo-code-generator-template/` — Templates - -Documentation : `doc/CODE_GENERATOR.md` - -## Documentation multilingue - -La documentation est bilingue (anglais/français) via **mmg** (Multilingual Markdown Generator). - -### Fonctionnement -- Les fichiers sources sont les `.base.md` (contiennent les deux langues) -- `mmg` génère : `FICHIER.md` (anglais) et `FICHIER.fr.md` (français) -- Marqueurs : ``, ``, `` (blocs de code partagés) - -### Commandes -```bash -make doc_markdown # Regénérer toute la doc multilingue -``` - -### Convention -- **Ne jamais modifier directement** les fichiers `.md` ou `.fr.md` générés -- Toujours modifier le fichier `.base.md` correspondant, puis exécuter `make doc_markdown` -- Les blocs de code vont dans ``, le texte dans `` et `` -- En-tête obligatoire dans chaque `.base.md` : -``` - - - - -``` - -### Fichiers concernés (30 fichiers) -- Racine : `README`, `CHANGELOG`, `TODO` -- `doc/` : DEVELOPMENT, PRODUCTION, DISCOVER, RUN, MIGRATION, WINDOWS_INSTALLATION, FAQ, GIT_REPO, POETRY, RELEASE, UPDATE, CONTRIBUTION, HOWTO, TODO, CODE_GENERATOR -- `docker/` : README -- `script/*/` : database, deployment, fork_github_repo, nginx, restful, selenium (2), todo, odoo/migration -- `.github/ISSUE_TEMPLATE/` : bug_report, feature_request - -## Internationalisation du CLI TODO (i18n) - -Le CLI interactif `script/todo/todo.py` supporte le français et l'anglais. - -### Architecture -- **`script/todo/todo_i18n.py`** — Module de traduction (dictionnaire `TRANSLATIONS`, fonctions `t()`, `get_lang()`, `set_lang()`) -- Les chaînes traduisibles utilisent `t("clé")` au lieu de texte en dur -- Les entrées de `todo.json` peuvent avoir un champ `prompt_description_key` résolu via `t()` (fallback sur `prompt_description`) - -### Résolution de la langue (priorité) -1. Variable d'environnement `EL_LANG` (définie dans `env_var.sh`, défaut `"fr"`) -2. Défaut : `"fr"` - -### Comportement -- Première exécution : prompt bilingue demande à l'utilisateur de choisir sa langue -- Le choix est persisté dans `env_var.sh` -- Changement de langue possible via le menu Execute > Langue/Language - -### Ajouter une traduction -1. Ajouter la clé dans `TRANSLATIONS` de `todo_i18n.py` avec les valeurs `"fr"` et `"en"` -2. Remplacer la chaîne en dur par `t("ma_clé")` dans `todo.py` -3. Pour les entrées JSON : ajouter `"prompt_description_key": "ma_clé"` dans `todo.json` - -## Déploiement - -- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6) -- **Systemd** : `script/systemd/` pour les services -- **Nginx** : `script/nginx/` pour le reverse proxy -- **SSL** : Certbot pour les certificats -- **DNS** : `script/deployment/update_dns_cloudflare.py` - -Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). - ## Points d'attention pour Claude - Toujours vérifier la version Odoo active avant de modifier du code (`cat .odoo-version`) @@ -270,58 +20,24 @@ Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch L - Pour la documentation : modifier les `.base.md`, jamais les `.md` ou `.fr.md` directement - Outil mmg disponible via `source .venv.erplibre/bin/activate && mmg` -## Workflow Orchestration - -### 1. Plan Mode Default -- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) -- If something goes sideways, STOP and re-plan immediately -- Don't keep pushing. -- Use plan mode for verification steps, not just building -- Write detailed specs upfront to reduce ambiguity - -### 2. Subagent Strategy -- Use subagents liberally to keep the main context window clean. -- Offload research, exploration, and parallel analysis to subagents -- For complex problems, throw more compute at it via subagents -- One task per subagent for focused execution - -### 3. Self-Improvement Loop -- Capture Lessons: Update AGENT.md with any change in approach the user has asked you to make. -- Write rules for yourself that prevent the same mistake -- Ruthlessly iterate on these lessons until the mistake rate drops. -- Review lessons at session start for relevant project - -### 4. Verification Before Done -- Never mark a task complete without proving it works -- Diff behavior between main and your changes when relevant -- Ask yourself, "Would a staff engineer approve this?" -- Run tests, check logs, and demonstrate correctness. - -### 5. Demand Elegance (Balanced) -- For non-trivial changes, pause and ask, "Is there a more elegant way?" -- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution" -- Skip this for simple, obvious fixes -- Don't over-engineer. -- Challenge your own work before presenting it - -### 6. Autonomous Bug Fixing -- When given a bug report, just fix it. Don't ask for hand-holding -- Point at logs, errors, and failing tests. -- Then resolve them. -- Zero context switching required from the user -- Go fix failing CI tests without being told how - -## Task Management - -1. **Plan First**: Write a plan to `tasks/todo.md` with checkable items. -2. **Verify Plan**: Check in before starting implementation. -3. **Track Progress**: Mark items complete as you go. -4. **Explain Changes**: High-level summary at each step. -5. **Document Results**: Add review section to `tasks/todo.md` -6. **Capture Lessons**: Update `tasks/lessons.md` after corrections - ## Core Principles - **Simplicity First**: Make every change as simple as possible. Impact minimal code. - **No Laziness**: Find root causes. No temporary fixes. Senior developer standards. - **Minimal Impact**: Changes should only touch what's necessary. Avoid introducing bugs. + +## Règles détaillées + +Les instructions détaillées sont dans `.claude/rules/` : + +| Fichier | Contenu | +|---------|---------| +| `01-versions.md` | Versions Odoo/Python/Poetry supportées | +| `02-project-structure.md` | Arborescence du projet | +| `03-commands.md` | Commandes essentielles (make, scripts) | +| `04-code-conventions.md` | Conventions Python, XML, fichiers, Git | +| `05-environments.md` | Venvs, pyenv, système de dépendances | +| `06-code-generator.md` | Génération de modules Odoo | +| `07-documentation.md` | Documentation multilingue (mmg) + i18n CLI | +| `08-deployment.md` | Docker, systemd, nginx, SSL, DNS | +| `09-workflow.md` | Workflow orchestration + task management | From 230c55c432499e442060c6ae04927c81596a0f51 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 02:51:01 -0400 Subject: [PATCH 3/9] [ADD] todo: add RTK management menu Integrate RTK (Rust Token Killer) into the interactive CLI to let users install, configure, and monitor token savings directly from the ERPLibre todo interface. Generated by Claude Code 2.1.74 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit --- script/todo/todo.py | 113 +++++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 65 ++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/script/todo/todo.py b/script/todo/todo.py index 88b2102..98a27d6 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -775,6 +775,7 @@ class TODO: print(f"🤖 {t('gpt_code_manage')}") choices = [ {"prompt_description": t("gpt_code_claude_commit")}, + {"prompt_description": t("menu_rtk")}, ] help_info = self.fill_help_info(choices) @@ -785,6 +786,8 @@ class TODO: return False elif status == "1": self._setup_claude_commit() + elif status == "2": + self.prompt_execute_rtk() else: print(t("cmd_not_found")) @@ -926,6 +929,116 @@ class TODO: ) print(t("kill_git_daemon_done")) + def prompt_execute_rtk(self): + print(f"🤖 {t('rtk_manage')}") + choices = [ + {"prompt_description": t("rtk_install")}, + {"prompt_description": t("rtk_version")}, + {"prompt_description": t("rtk_gain")}, + {"prompt_description": t("rtk_discover")}, + {"prompt_description": t("rtk_init_global")}, + {"prompt_description": t("rtk_status")}, + ] + help_info = self.fill_help_info(choices) + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + elif status == "1": + self.rtk_install() + elif status == "2": + self.rtk_check_version() + elif status == "3": + self.rtk_show_gain() + elif status == "4": + self.rtk_discover() + elif status == "5": + self.rtk_init_global() + elif status == "6": + self.rtk_check_status() + else: + print(t("cmd_not_found")) + + def rtk_install(self): + print(f"🤖 {t('rtk_install_method')}") + choices = [ + {"prompt_description": t("rtk_install_curl")}, + {"prompt_description": t("rtk_install_brew")}, + {"prompt_description": t("rtk_install_cargo")}, + ] + help_info = self.fill_help_info(choices) + status = click.prompt(help_info) + print() + if status == "0": + return + elif status == "1": + self.execute.exec_command_live( + "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", + source_erplibre=False, + ) + elif status == "2": + self.execute.exec_command_live( + "brew install rtk", + source_erplibre=False, + ) + elif status == "3": + self.execute.exec_command_live( + "cargo install --git https://github.com/rtk-ai/rtk", + source_erplibre=False, + ) + else: + print(t("cmd_not_found")) + + def rtk_check_version(self): + self.execute.exec_command_live( + "rtk --version", + source_erplibre=False, + ) + + def rtk_show_gain(self): + self.execute.exec_command_live( + "rtk gain", + source_erplibre=False, + ) + + def rtk_discover(self): + self.execute.exec_command_live( + "rtk discover", + source_erplibre=False, + ) + + def rtk_init_global(self): + self.execute.exec_command_live( + "rtk init --global", + source_erplibre=False, + ) + + def rtk_check_status(self): + rtk_path = shutil.which("rtk") + if rtk_path is None: + print(t("rtk_not_installed")) + return + + result = self.execute.exec_command_live( + "rtk --version", + source_erplibre=False, + quiet=True, + return_status_and_output=True, + ) + if isinstance(result, tuple) and result[0] == 0: + version_output = " ".join(result[1]).strip() + print(f"{t('rtk_installed_version')}{version_output}") + else: + print(f"{t('rtk_installed_version')}?") + + config_path = os.path.expanduser("~/.config/rtk/config.toml") + if os.path.exists(config_path): + print(t("rtk_hook_active")) + else: + print(t("rtk_hook_inactive")) + def prompt_execute_config(self): print(f"🤖 {t('config_manage')}") choices = [ diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index a4f3ca3..9310329 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -96,10 +96,75 @@ TRANSLATIONS = { "fr": "Sécurité - Audit de sécurité des dépendances", "en": "Security - Dependency security audit", }, + "menu_rtk": { + "fr": "RTK - Proxy CLI pour réduire la consommation de tokens LLM", + "en": "RTK - CLI proxy to reduce LLM token consumption", + }, "menu_lang": { "fr": "Langue - Changer la langue / Change language", "en": "Language - Change language / Changer la langue", }, + # RTK (Rust Token Killer) + "rtk_manage": { + "fr": "Gérer RTK (Rust Token Killer) pour optimiser les tokens!", + "en": "Manage RTK (Rust Token Killer) for token optimization!", + }, + "rtk_install": { + "fr": "Installer RTK", + "en": "Install RTK", + }, + "rtk_version": { + "fr": "Vérifier la version de RTK", + "en": "Check RTK version", + }, + "rtk_gain": { + "fr": "Afficher les économies de tokens cumulées", + "en": "Show cumulative token savings", + }, + "rtk_discover": { + "fr": "Identifier les opportunités d'optimisation", + "en": "Discover optimization opportunities", + }, + "rtk_init_global": { + "fr": "Initialiser le hook auto-rewrite global", + "en": "Initialize global auto-rewrite hook", + }, + "rtk_status": { + "fr": "Vérifier le statut de RTK", + "en": "Check RTK status", + }, + "rtk_not_installed": { + "fr": "RTK n'est pas installé. Utilisez l'option 1 pour l'installer.", + "en": "RTK is not installed. Use option 1 to install it.", + }, + "rtk_installed_version": { + "fr": "RTK est installé, version : ", + "en": "RTK is installed, version: ", + }, + "rtk_hook_active": { + "fr": "Hook auto-rewrite global : actif", + "en": "Global auto-rewrite hook: active", + }, + "rtk_hook_inactive": { + "fr": "Hook auto-rewrite global : inactif", + "en": "Global auto-rewrite hook: inactive", + }, + "rtk_install_method": { + "fr": "Méthode d'installation :", + "en": "Installation method:", + }, + "rtk_install_curl": { + "fr": "curl - Script d'installation automatique", + "en": "curl - Automatic install script", + }, + "rtk_install_brew": { + "fr": "brew - Homebrew (macOS/Linux)", + "en": "brew - Homebrew (macOS/Linux)", + }, + "rtk_install_cargo": { + "fr": "cargo - Compilation depuis les sources (Rust requis)", + "en": "cargo - Build from source (Rust required)", + }, # Prompts and messages "enter_password": { "fr": "Entrez votre mot de passe : ", From c2e08f1de0da7211be0fbc00c9517168fc306eb8 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 04:04:52 -0400 Subject: [PATCH 4/9] [IMP] todo: add git remote, vim config and automation Add git remote add command with default name "localhost", git editor vim configuration via todo.json, and a Claude automation tool to dynamically add commands to todo.json. Support new bash_command key in execute_from_configuration. Generated by Claude Code 2.1.74 model claude-opus-4-6 Co-Authored-By: Mathieu Benoit --- script/todo/todo.json | 7 +++- script/todo/todo.py | 70 ++++++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 63 ++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/script/todo/todo.json b/script/todo/todo.json index 90a52a7..256c02b 100644 --- a/script/todo/todo.json +++ b/script/todo/todo.json @@ -52,5 +52,10 @@ "makefile_cmd": "format" } ], - "git_from_makefile": [] + "git_from_makefile": [ + { + "prompt_description_key": "git_config_vim", + "bash_command": "git config --global core.editor \"vim\"" + } + ] } diff --git a/script/todo/todo.py b/script/todo/todo.py index 98a27d6..f646eb7 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -456,6 +456,13 @@ class TODO: db_name, extra_cmd_web_login=extra_cmd_web_login ) + bash_command = instance.get("bash_command") + if bash_command: + print(f"{t('will_execute')} {bash_command}") + self.execute.exec_command_live( + bash_command, source_erplibre=False + ) + command = instance.get("command") if command: self.prompt_execute_selenium( @@ -661,6 +668,7 @@ class TODO: print(f"🤖 {t('git_manage')}") choices = [ {"prompt_description": t("git_local_server")}, + {"prompt_description": t("git_add_remote")}, ] # Append config-driven entries @@ -677,6 +685,8 @@ class TODO: return False elif status == "1": self.prompt_execute_git_local_server() + elif status == "2": + self._git_add_remote() else: cmd_no_found = True try: @@ -690,6 +700,24 @@ class TODO: if cmd_no_found: print(t("cmd_not_found")) + def _git_add_remote(self): + remote_name = ( + input(t("git_add_remote_name_prompt")).strip() or "localhost" + ) + remote_url = input(t("git_add_remote_url_prompt")).strip() + if not remote_url: + print(t("git_add_remote_url_required")) + return + cmd = f"git remote add {remote_name} {remote_url}" + print(f"{t('will_execute')} {cmd}") + try: + self.execute.exec_command_live( + cmd, source_erplibre=False + ) + print(t("git_add_remote_success")) + except Exception as e: + print(f"{t('git_add_remote_error')}{e}") + def prompt_execute_git_local_server(self): print(f"🤖 {t('git_repo_manage')}") choices = [ @@ -775,6 +803,7 @@ class TODO: print(f"🤖 {t('gpt_code_manage')}") choices = [ {"prompt_description": t("gpt_code_claude_commit")}, + {"prompt_description": t("gpt_code_claude_add_automation")}, {"prompt_description": t("menu_rtk")}, ] help_info = self.fill_help_info(choices) @@ -787,6 +816,8 @@ class TODO: elif status == "1": self._setup_claude_commit() elif status == "2": + self._claude_add_automation() + elif status == "3": self.prompt_execute_rtk() else: print(t("cmd_not_found")) @@ -830,6 +861,45 @@ class TODO: except Exception as e: print(f"{t('gpt_code_commit_error')}{e}") + def _claude_add_automation(self): + description = input( + t("gpt_code_claude_add_automation_prompt") + ).strip() + if not description: + return + command = input( + t("gpt_code_claude_add_automation_cmd_prompt") + ).strip() + if not command: + return + section = ( + input( + t("gpt_code_claude_add_automation_section_prompt") + ).strip() + or "git" + ) + section_key = f"{section}_from_makefile" + config_path = os.path.join( + os.path.dirname(__file__), "todo.json" + ) + try: + with open(config_path) as f: + config = json.load(f) + if section_key not in config: + config[section_key] = [] + config[section_key].append( + { + "prompt_description": description, + "bash_command": command, + } + ) + with open(config_path, "w") as f: + json.dump(config, f, indent=4, ensure_ascii=False) + f.write("\n") + print(t("gpt_code_claude_add_automation_success")) + except Exception as e: + print(f"{t('gpt_code_claude_add_automation_error')}{e}") + def prompt_execute_doc(self): print(f"🤖 {t('doc_search')}") choices = [ diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 9310329..77387f7 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -558,6 +558,69 @@ TRANSLATIONS = { "fr": "Serve - Démarrer le daemon git", "en": "Serve - Start git daemon", }, + # Git remote add + "git_add_remote": { + "fr": "Ajouter un remote vers un dépôt local", + "en": "Add a remote to a local repository", + }, + "git_add_remote_name_prompt": { + "fr": "Nom du remote (défaut: localhost) : ", + "en": "Remote name (default: localhost): ", + }, + "git_add_remote_url_prompt": { + "fr": "Adresse du dépôt (ex: git://192.168.1.100/mon-repo.git) : ", + "en": "Repository address (e.g.: git://192.168.1.100/my-repo.git): ", + }, + "git_add_remote_url_required": { + "fr": "L'adresse du dépôt est requise!", + "en": "Repository address is required!", + }, + "git_add_remote_success": { + "fr": "Remote ajouté avec succès!", + "en": "Remote added successfully!", + }, + "git_add_remote_error": { + "fr": "Erreur lors de l'ajout du remote : ", + "en": "Error adding remote: ", + }, + # Git config vim + "git_config_vim": { + "fr": "Configuration git local par vim", + "en": "Configure git local editor to vim", + }, + "git_config_vim_success": { + "fr": "Éditeur git configuré sur vim avec succès!", + "en": "Git editor configured to vim successfully!", + }, + "git_config_vim_error": { + "fr": "Erreur lors de la configuration : ", + "en": "Error during configuration: ", + }, + # GPT code - Claude automation + "gpt_code_claude_add_automation": { + "fr": "Ajouter une automatisation avec Claude dans todo.py", + "en": "Add an automation with Claude in todo.py", + }, + "gpt_code_claude_add_automation_prompt": { + "fr": "Description de la commande à ajouter : ", + "en": "Description of the command to add: ", + }, + "gpt_code_claude_add_automation_cmd_prompt": { + "fr": "Commande bash à exécuter : ", + "en": "Bash command to execute: ", + }, + "gpt_code_claude_add_automation_section_prompt": { + "fr": "Section du menu (git/code/config/network/process) : ", + "en": "Menu section (git/code/config/network/process): ", + }, + "gpt_code_claude_add_automation_success": { + "fr": "Automatisation ajoutée avec succès dans todo.json!", + "en": "Automation added successfully in todo.json!", + }, + "gpt_code_claude_add_automation_error": { + "fr": "Erreur lors de l'ajout de l'automatisation : ", + "en": "Error adding automation: ", + }, # Language selection "lang_prompt": { "fr": "Choisir la langue / Choose language", From 91c835d57e95e82d328f1f9fe8f8b6b479f7f2c9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 04:37:19 -0400 Subject: [PATCH 5/9] [IMP] todo: add Claude configs submenu and command listing Refactor single commit setup into a generic deployment mechanism for Claude commands, allowing easy addition of new commands. Add todo_add_command template and an option to list installed custom commands with their dates. Generated by Claude Code 2.1.74 model claude-opus-4-6 Co-Authored-By: Mathieu Benoit --- ...mplate_claude_commands_todo_add_command.md | 143 ++++++++++++++++++ script/todo/todo.py | 114 +++++++++++--- script/todo/todo_i18n.py | 52 ++++++- 3 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 conf/template_claude_commands_todo_add_command.md diff --git a/conf/template_claude_commands_todo_add_command.md b/conf/template_claude_commands_todo_add_command.md new file mode 100644 index 0000000..dce3469 --- /dev/null +++ b/conf/template_claude_commands_todo_add_command.md @@ -0,0 +1,143 @@ +--- +name: todo_add_command +description: "Add a new menu command to script/todo/todo.py with i18n support and optional todo.json entry." +allowed-tools: + - Read + - Edit + - Write + - Grep + - Glob + - Bash(python3 -m py_compile:*) + - Bash(python3 -c:*) +--- + +## Context + +- Current todo.py menu structure: !`grep -n "def prompt_execute" script/todo/todo.py | head -20` +- Current todo.json sections: !`python3 -c "import json; d=json.load(open('script/todo/todo.json')); print('\n'.join(d.keys()))"` +- Current i18n keys count: !`grep -c fr.: script/todo/todo_i18n.py` + +## Architecture Reference + +### Files to modify + +| File | Role | +|------|------| +| `script/todo/todo.py` | Main CLI — menu methods, business logic | +| `script/todo/todo_i18n.py` | Translations dict `TRANSLATIONS` with `"fr"` and `"en"` keys | +| `script/todo/todo.json` | Config-driven entries (optional, for `bash_command` or `makefile_cmd`) | + +### Pattern A — Hardcoded menu entry (interactive logic) + +Used when the command needs Python logic (user prompts, conditionals, API calls). + +1. **Add i18n keys** in `todo_i18n.py` `TRANSLATIONS` dict: +```python +"my_feature_description": { + "fr": "Description en français", + "en": "English description", +}, +``` + +2. **Add choice** in the parent menu method (e.g. `prompt_execute_git`): +```python +choices = [ + ..., + {"prompt_description": t("my_feature_description")}, +] +``` + +3. **Add elif branch** in the same menu's while loop: +```python +elif status == "N": + self._my_feature() +``` + +4. **Add method** to the `TODO` class: +```python +def _my_feature(self): + # Implementation here + pass +``` + +### Pattern B — Config-driven entry (simple bash command) + +Used when the command just runs a bash command or a make target. + +1. **Add i18n key** in `todo_i18n.py` (use `prompt_description_key`). + +2. **Add entry** in `todo.json` under the appropriate `*_from_makefile` section: +```json +{ + "prompt_description_key": "my_i18n_key", + "bash_command": "my-command --flag" +} +``` +Or for make targets: +```json +{ + "prompt_description_key": "my_i18n_key", + "makefile_cmd": "my_make_target" +} +``` + +These are automatically picked up by `execute_from_configuration()`. + +### Available menu sections + +| Menu | Method | JSON key | +|------|--------|----------| +| Automation | `prompt_execute_function` | `function` | +| Code | `prompt_execute_code` | `code_from_makefile` | +| Config | `prompt_execute_config` | — | +| Database | `prompt_execute_database` | — | +| Doc | `prompt_execute_doc` | — | +| Git | `prompt_execute_git` | `git_from_makefile` | +| GPT code | `prompt_execute_gpt_code` | — | +| Network | `prompt_execute_network` | — | +| Process | `prompt_execute_process` | — | +| Run | `prompt_execute_instance` | `instance` | +| Security | `prompt_execute_security` | — | +| Test | `prompt_execute_test` | — | +| Update | `prompt_execute_update` | `update_from_makefile` | + +### Key conventions + +- i18n keys: `snake_case`, grouped by section with a comment header +- Method names: `_private_method` for actions, `prompt_execute_*` for submenus +- All user-facing strings must use `t("key")` — never hardcoded text +- Use `self.execute.exec_command_live(cmd, source_erplibre=False)` to run bash +- Use `input(t("prompt_key")).strip()` for user input +- Use `click.prompt(help_info)` for menu navigation +- Menu methods return `False` on back, use `self.fill_help_info(choices)` for formatting + +## Task + +Add a new command to the todo.py menu system. The user will describe what they want. + +### Steps + +1. **Determine the target menu section** from the user's description +2. **Choose Pattern A or B** based on complexity: + - Simple bash/make command → Pattern B (todo.json entry) + - Needs user interaction or logic → Pattern A (hardcoded method) +3. **Add i18n translations** in `todo_i18n.py` — always both `"fr"` and `"en"` +4. **Implement the feature** following the appropriate pattern +5. **Validate syntax**: + ```bash + python3 -m py_compile script/todo/todo.py + python3 -m py_compile script/todo/todo_i18n.py + python3 -c "import json; json.load(open('script/todo/todo.json'))" + ``` + +### Rules + +- Keep menu numbering sequential — update all `elif` branches if inserting +- Group i18n keys with a `# Section name` comment +- Match existing code style (Black, 79 char lines for Odoo modules) +- Do not break existing menu entries or their numbering +- Test that `fill_help_info` and `execute_from_configuration` handle the new entry + +## User Request + +$ARGUMENTS diff --git a/script/todo/todo.py b/script/todo/todo.py index f646eb7..2be6d41 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -802,7 +802,7 @@ class TODO: def prompt_execute_gpt_code(self): print(f"🤖 {t('gpt_code_manage')}") choices = [ - {"prompt_description": t("gpt_code_claude_commit")}, + {"prompt_description": t("gpt_code_claude_configs")}, {"prompt_description": t("gpt_code_claude_add_automation")}, {"prompt_description": t("menu_rtk")}, ] @@ -814,7 +814,7 @@ class TODO: if status == "0": return False elif status == "1": - self._setup_claude_commit() + self._prompt_claude_configs() elif status == "2": self._claude_add_automation() elif status == "3": @@ -822,44 +822,118 @@ class TODO: else: print(t("cmd_not_found")) - def _setup_claude_commit(self): + def _prompt_claude_configs(self): + print(f"🤖 {t('gpt_code_claude_configs_manage')}") + choices = [ + {"prompt_description": t("gpt_code_claude_commit")}, + { + "prompt_description": t( + "gpt_code_claude_todo_add_command" + ) + }, + { + "prompt_description": t( + "gpt_code_claude_list_commands" + ) + }, + ] + help_info = self.fill_help_info(choices) + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + elif status == "1": + self._setup_claude_command( + "commit", + "template_claude_commands_commit.md", + personalize=True, + ) + elif status == "2": + self._setup_claude_command( + "todo_add_command", + "template_claude_commands_todo_add_command.md", + ) + elif status == "3": + self._list_claude_commands() + else: + print(t("cmd_not_found")) + + def _list_claude_commands(self): + commands_dir = os.path.expanduser("~/.claude/commands") + if not os.path.isdir(commands_dir): + print(t("gpt_code_claude_no_commands")) + return + files = sorted( + f + for f in os.listdir(commands_dir) + if f.endswith(".md") + ) + if not files: + print(t("gpt_code_claude_no_commands")) + return + print(t("gpt_code_claude_list_header")) + print("-" * 50) + for f in files: + filepath = os.path.join(commands_dir, f) + mtime = os.path.getmtime(filepath) + date_str = datetime.datetime.fromtimestamp(mtime).strftime( + "%Y-%m-%d %H:%M" + ) + name = f[:-3] # remove .md + print(f" /{name:<30} {date_str}") + print("-" * 50) + print( + f"{t('gpt_code_claude_list_total')}" + f" {len(files)}" + ) + + def _setup_claude_command( + self, command_name, template_filename, personalize=False + ): dest_dir = os.path.expanduser("~/.claude/commands") - dest_file = os.path.join(dest_dir, "commit.md") + dest_file = os.path.join(dest_dir, f"{command_name}.md") if os.path.exists(dest_file): - print(t("gpt_code_commit_exists")) - return - - name = input(t("gpt_code_enter_name")).strip() - email = input(t("gpt_code_enter_email")).strip() + print(f"{t('gpt_code_cmd_exists')}{dest_file}") + overwrite = input( + t("gpt_code_cmd_overwrite") + ).strip() + if overwrite not in ("y", "Y"): + print(t("gpt_code_cmd_nothing_to_do")) + return template_path = os.path.join( os.path.dirname(__file__), "..", "..", "conf", - "template_claude_commands_commit.md", + template_filename, ) try: with open(template_path) as f: content = f.read() - content = content.replace( - "Your Name ", - f"{name} <{email}>", - ) - content = content.replace( - "Your Name ", - f"{name} ", - ) + if personalize: + name = input(t("gpt_code_enter_name")).strip() + email = input(t("gpt_code_enter_email")).strip() + content = content.replace( + "Your Name ", + f"{name} <{email}>", + ) + content = content.replace( + "Your Name ", + f"{name} ", + ) os.makedirs(dest_dir, exist_ok=True) with open(dest_file, "w") as f: f.write(content) - print(t("gpt_code_commit_created")) + print(f"{t('gpt_code_cmd_created')}{dest_file}") except Exception as e: - print(f"{t('gpt_code_commit_error')}{e}") + print(f"{t('gpt_code_cmd_error')}{e}") def _claude_add_automation(self): description = input( diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 77387f7..f75c2b5 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -655,9 +655,17 @@ TRANSLATIONS = { "fr": "Outils d'assistant IA pour le développement!", "en": "AI assistant tools for development!", }, + "gpt_code_claude_configs": { + "fr": "Configurer les configurations Claude Code", + "en": "Configure Claude Code configurations", + }, "gpt_code_claude_commit": { - "fr": "Configurer le commit Claude Code", - "en": "Configure Claude Code commit", + "fr": "Commit - Commande de commit OCA/Odoo", + "en": "Commit - OCA/Odoo commit command", + }, + "gpt_code_claude_todo_add_command": { + "fr": "Todo Add Command - Ajouter une commande au menu todo.py", + "en": "Todo Add Command - Add a command to todo.py menu", }, "gpt_code_enter_name": { "fr": "Entrez votre nom complet : ", @@ -667,6 +675,46 @@ TRANSLATIONS = { "fr": "Entrez votre courriel : ", "en": "Enter your email: ", }, + "gpt_code_claude_configs_manage": { + "fr": "Déployer les commandes Claude Code!", + "en": "Deploy Claude Code commands!", + }, + "gpt_code_claude_list_commands": { + "fr": "Afficher les commandes personnalisées installées", + "en": "Show installed custom commands", + }, + "gpt_code_claude_no_commands": { + "fr": "Aucune commande personnalisée trouvée dans ~/.claude/commands/", + "en": "No custom commands found in ~/.claude/commands/", + }, + "gpt_code_claude_list_header": { + "fr": "Commandes personnalisées Claude Code :", + "en": "Claude Code custom commands:", + }, + "gpt_code_claude_list_total": { + "fr": "Total :", + "en": "Total:", + }, + "gpt_code_cmd_exists": { + "fr": "Le fichier existe déjà : ", + "en": "File already exists: ", + }, + "gpt_code_cmd_overwrite": { + "fr": "Voulez-vous écraser le fichier? (y/Y) : ", + "en": "Do you want to overwrite the file? (y/Y): ", + }, + "gpt_code_cmd_nothing_to_do": { + "fr": "Rien à faire.", + "en": "Nothing to do.", + }, + "gpt_code_cmd_created": { + "fr": "Fichier créé avec succès : ", + "en": "File created successfully: ", + }, + "gpt_code_cmd_error": { + "fr": "Erreur lors de la création du fichier : ", + "en": "Error creating file: ", + }, "gpt_code_commit_exists": { "fr": "Le fichier ~/.claude/commands/commit.md existe déjà. Aucune action effectuée.", "en": "File ~/.claude/commands/commit.md already exists. No action taken.", From 6fe2de73b222e50b87fca68263f3676bcba052b3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 05:20:32 -0400 Subject: [PATCH 6/9] [UPD] script execute with more space for command display --- script/execute/execute.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/execute/execute.py b/script/execute/execute.py index 887a8e9..62ddcb8 100644 --- a/script/execute/execute.py +++ b/script/execute/execute.py @@ -115,7 +115,7 @@ class Execute: command = self.cmd_source_default % command if not quiet: - print("🏠 ⬇ Execute command :") + print("🏠 ⬇ Execute command :\n") print(command) output_lines = [] @@ -172,10 +172,10 @@ class Execute: duration_delta = datetime.timedelta(seconds=duration_sec) human_time = humanize.precisedelta(duration_delta) if not quiet: - print(f"🏠 ⬆ Executed ({human_time}) :") + print(f"🏠 ⬆ Executed ({human_time}) :\n") else: if not quiet: - print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :") + print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :\n") if not quiet: print(command) print() From 4fc15c306b3a7e4b5fb6c2779484dbce2748eb01 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 05:31:45 -0400 Subject: [PATCH 7/9] [REF] todo: use English text as i18n keys Replace snake_case i18n keys with their English translation so developers can read the code without cross-referencing the translations dictionary. Remove 5 obsolete duplicate entries. Generated by Claude Code 2.1.74 model claude-opus-4-6 Co-Authored-By: Mathieu Benoit --- script/todo/todo.json | 18 +- script/todo/todo.py | 398 +++++++++++++++++++-------------------- script/todo/todo_i18n.py | 358 +++++++++++++++++------------------ 3 files changed, 379 insertions(+), 395 deletions(-) diff --git a/script/todo/todo.json b/script/todo/todo.json index 256c02b..27255b0 100644 --- a/script/todo/todo.json +++ b/script/todo/todo.json @@ -10,51 +10,51 @@ }, "instance": [ { - "prompt_description_key": "json_instance_test", + "prompt_description_key": "Test - Minimal base instance", "makefile_cmd": "db_restore_erplibre_base_db_test", "database": "test" }, { - "prompt_description_key": "json_robot_minimal", + "prompt_description_key": "Open RobotLibre 🤖 minimal", "makefile_cmd": "robot_libre", "database": "robotlibre" }, { - "prompt_description_key": "json_robot_search", + "prompt_description_key": "Open RobotLibre 🤖 with search enabled", "makefile_cmd": "robot_libre_all", "database": "robotlibre" } ], "function": [ { - "prompt_description_key": "json_open_erplibre_todo", + "prompt_description_key": "Open ERPLibre with TODO 🤖", "command": "./.venv.erplibre/bin/python ./script/selenium/web_login.py" } ], "update_from_makefile": [ { - "prompt_description_key": "json_update_erplibre_base_test", + "prompt_description_key": "Update all erplibre_base on database test", "makefile_cmd": "db_erplibre_base_db_test_update_all", "database": "test" } ], "code_from_makefile": [ { - "prompt_description_key": "json_show_code_status", + "prompt_description_key": "Show code status", "makefile_cmd": "repo_show_status" }, { - "prompt_description_key": "json_stash_all_code", + "prompt_description_key": "Stash all code", "makefile_cmd": "repo_do_stash" }, { - "prompt_description_key": "json_format_modified_code", + "prompt_description_key": "Format modified code", "makefile_cmd": "format" } ], "git_from_makefile": [ { - "prompt_description_key": "git_config_vim", + "prompt_description_key": "Configure git local editor to vim", "bash_command": "git config --global core.editor \"vim\"" } ] diff --git a/script/todo/todo.py b/script/todo/todo.py index 2be6d41..d9a9790 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -100,33 +100,33 @@ class TODO: def _change_language(self): print() - print(t("lang_prompt") + ":") - print(f"[1] {t('lang_french')}") - print(f"[2] {t('lang_english')}") - print(f"[0] {t('back')}") + print(t("Choose language / Choisir la langue") + ":") + print(f"[1] {t('French')}") + print(f"[2] {t('English')}") + print(f"[0] {t('Back')}") choice = "" while choice not in ("0", "1", "2"): - choice = input(t("selection")).strip() + choice = input(t("Select: ")).strip() if choice == "0": return False elif choice == "1": set_lang("fr") else: set_lang("en") - print(t("lang_changed")) + print(t("Language changed to: English")) def run(self): with open(self.config_file.get_logo_ascii_file_path()) as my_file: print(my_file.read()) self._ask_language() - print(t("opening")) - print(f"🤖 {t('enter_directives')}") - help_info = f"""{t("command")} -[1] {t("menu_execute")} -[2] {t("menu_install")} -[3] {t("menu_question")} -[4] {t("menu_fork")} -[0] {t("menu_quit")} + print(t("Opening TODO ...")) + print(f"🤖 {t('=> Enter your choice by number and press Enter!')}") + help_info = f"""{t("Command:")} +[1] {t("Execute")} +[2] {t("Install")} +[3] {t("Question")} +[4] {t("Fork - Open TODO in a new tab")} +[0] {t("Quit")} """ while True: try: @@ -160,16 +160,16 @@ class TODO: # elif status == "3" or status == "install": # print("install") else: - print(t("cmd_not_found")) + print(t("Command not found !")) print(status) # manipuler() def execute_prompt_ia(self): while True: - help_info = f"""{t("command")} -[0] {t("back")} -{t("ia_prompt")}""" + help_info = f"""{t("Command:")} +[0] {t("Back")} +{t("Write your question ")}""" status = click.prompt(help_info) print() if status == "0": @@ -193,22 +193,22 @@ class TODO: print() def prompt_execute(self): - help_info = f"""{t("command")} -[1] {t("menu_automation")} -[2] {t("menu_code")} -[3] {t("menu_config")} -[4] {t("menu_database")} -[5] {t("menu_doc")} -[6] {t("menu_git")} -[7] {t("menu_gpt_code")} -[8] {t("menu_lang")} -[9] {t("menu_network")} -[10] {t("menu_process")} -[11] {t("menu_run")} -[12] {t("menu_security")} -[13] {t("menu_test")} -[14] {t("menu_update")} -[0] {t("back")} + help_info = f"""{t("Command:")} +[1] {t("Automation - Demonstration of developed features")} +[2] {t("Code - Developer tools")} +[3] {t("Config - Configuration file management")} +[4] {t("Database - Database tools")} +[5] {t("Doc - Documentation search")} +[6] {t("Git - Git tools")} +[7] {t("GPT code - AI assistant tools")} +[8] {t("Language - Change language / Changer la langue")} +[9] {t("Network - Network tools")} +[10] {t("Process - Execution tools")} +[11] {t("Run - Execute and install an instance")} +[12] {t("Security - Dependency security audit")} +[13] {t("Test - Test an Odoo module")} +[14] {t("Update - Update all developed staging source code")} +[0] {t("Back")} """ while True: status = click.prompt(help_info) @@ -272,7 +272,7 @@ class TODO: if status is not False: return else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_install(self): print("Detect first installation from code source.") @@ -352,7 +352,7 @@ class TODO: ), "0": ( "0", - f"0: {t('menu_quit')}", + f"0: {t('Quit')}", ), } commands_end = {} @@ -370,7 +370,7 @@ class TODO: label += " - Installed" if odoo_version == odoo_installed_version: label += " - Actual" - if version_info.get("default"): + if version_info.get("Default"): label += " - Default" if version_info.get("is_deprecated"): label += " - Deprecated" @@ -388,11 +388,11 @@ class TODO: odoo_version_input = "" while odoo_version_input not in install_commands: if odoo_version_input: - print(f"{t('error_value')} '{odoo_version_input}'") + print(f"{t('Error, cannot understand value')} '{odoo_version_input}'") str_input_dyn_odoo_version = ( - f"💬 {t('choose_version')}\n\t" + f"💬 {t('Choose a version:')}\n\t" + "\n\t".join([a[1] for a in install_commands.values()]) - + f"\n{t('selection')}" + + f"\n{t('Select: ')}" ) odoo_version_input = ( input(str_input_dyn_odoo_version).strip().lower() @@ -402,7 +402,7 @@ class TODO: return cmd_intern = install_commands.get(odoo_version_input)[2] - print(f"{t('will_execute')}\n{cmd_intern}") + print(f"{t('Will execute:')}\n{cmd_intern}") # TODO use external script to detect terminal to use on system # TODO check script open_terminal_code_generator.sh @@ -412,7 +412,7 @@ class TODO: cmd_intern, shell=True, executable="/bin/bash", check=True ) except subprocess.CalledProcessError as e: - print(f"{t('script_failed')} {e.returncode}.") + print(f"{t('The Bash script failed with return code')} {e.returncode}.") print("Wait after installation and open projects by terminal.") print("make open_terminal") self.restart_script(str(e)) @@ -458,12 +458,12 @@ class TODO: bash_command = instance.get("bash_command") if bash_command: - print(f"{t('will_execute')} {bash_command}") + print(f"{t('Will execute:')} {bash_command}") self.execute.exec_command_live( bash_command, source_erplibre=False ) - command = instance.get("command") + command = instance.get("Command:") if command: self.prompt_execute_selenium( command=command, extra_cmd_web_login=extra_cmd_web_login @@ -474,8 +474,8 @@ class TODO: callback(instance) def fill_help_info(self, choices): - help_info = t("command") + "\n" - help_end = f"[0] {t('back')}\n" + help_info = t("Command:") + "\n" + help_end = f"[0] {t('Back')}\n" for i, instance in enumerate(choices): desc_key = instance.get("prompt_description_key") if desc_key: @@ -496,14 +496,14 @@ class TODO: # Support mobile ERPLibre if os.path.exists(MOBILE_HOME_PATH): menu_entry = { - "prompt_description": t("mobile_compile_run"), + "prompt_description": t("Mobile - Compile and run software"), "callback": self.callback_make_mobile_home, } choices.append(menu_entry) # Support custom database to execute menu_entry = { - "prompt_description": t("choose_database"), + "prompt_description": t("Choose your database"), "callback": self.callback_execute_custom_database, } choices.insert(0, menu_entry) @@ -520,7 +520,7 @@ class TODO: int_cmd = int(status) if 1 < int_cmd <= init_len: cmd_no_found = False - status = click.confirm(t("new_instance_confirm")) + status = click.confirm(t("Do you want a new instance?")) instance = choices[int_cmd - 1] self.execute_from_configuration( instance, @@ -537,7 +537,7 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_function(self): choices = self.config_file.get_config("function") @@ -559,11 +559,11 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_update(self): # self.execute.exec_command_live(f"make {makefile_cmd}") - print(f"🤖 {t('update_dev')}") + print(f"🤖 {t('Development update')}") # TODO détecter les modules en modification pour faire la mise à jour en cours # TODO demander sur quel BD faire la mise à jour # TODO proposer les modules manuelles selon la configuration à mettre à jour @@ -574,11 +574,11 @@ class TODO: choices = self.config_file.get_config("update_from_makefile") menu_entry = { - "prompt_description": t("upgrade_odoo_migration"), + "prompt_description": t("Upgrade Odoo - Migration Database"), } choices.append(menu_entry) poetry_entry = { - "prompt_description": t("upgrade_poetry_dependency"), + "prompt_description": t("Upgrade Poetry - Dependency of Odoo"), } choices.append(poetry_entry) help_info = self.fill_help_info(choices) @@ -604,10 +604,10 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_code(self): - print(f"🤖 {t('code_need')}") + print(f"🤖 {t('What do you need for development?')}") # help_info = """Commande : # [1] Status Git local et distant # [2] Démarrer le générateur de code @@ -623,18 +623,18 @@ class TODO: choices = self.config_file.get_config("code_from_makefile") menu_entry = { - "prompt_description": t("open_shell"), + "prompt_description": t("Open SHELL"), } choices.append(menu_entry) menu_entry = { - "prompt_description": t("upgrade_module"), + "prompt_description": t("Upgrade Module"), } choices.append(menu_entry) choices.append( { - "prompt_description": t("debug"), + "prompt_description": t("Debug"), } ) @@ -662,13 +662,13 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_git(self): - print(f"🤖 {t('git_manage')}") + print(f"🤖 {t('Git management tools!')}") choices = [ - {"prompt_description": t("git_local_server")}, - {"prompt_description": t("git_add_remote")}, + {"prompt_description": t("Local git server")}, + {"prompt_description": t("Add a remote to a local repository")}, ] # Append config-driven entries @@ -698,31 +698,31 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def _git_add_remote(self): remote_name = ( - input(t("git_add_remote_name_prompt")).strip() or "localhost" + input(t("Remote name (default: localhost): ")).strip() or "localhost" ) - remote_url = input(t("git_add_remote_url_prompt")).strip() + remote_url = input(t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ")).strip() if not remote_url: - print(t("git_add_remote_url_required")) + print(t("Repository address is required!")) return cmd = f"git remote add {remote_name} {remote_url}" - print(f"{t('will_execute')} {cmd}") + print(f"{t('Will execute:')} {cmd}") try: self.execute.exec_command_live( cmd, source_erplibre=False ) - print(t("git_add_remote_success")) + print(t("Remote added successfully!")) except Exception as e: - print(f"{t('git_add_remote_error')}{e}") + print(f"{t('Error adding remote: ')}{e}") def prompt_execute_git_local_server(self): - print(f"🤖 {t('git_repo_manage')}") + print(f"🤖 {t('Manage local git repository server!')}") choices = [ - {"prompt_description": t("git_repo_deploy_local")}, - {"prompt_description": t("git_repo_deploy_production")}, + {"prompt_description": t("Deploy a local git server (~/.git-server)")}, + {"prompt_description": t("Deploy a production git server (/srv/git, root required)")}, ] help_info = self.fill_help_info(choices) @@ -736,21 +736,21 @@ class TODO: elif status == "2": self._prompt_git_server_actions(production_ready=True) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _prompt_git_server_actions(self, production_ready=False): mode = ( - t("git_mode_production") + t("Production mode (/srv/git, root required)") if production_ready - else t("git_mode_local") + else t("Local mode (~/.git-server)") ) print(f"🤖 {mode}") choices = [ - {"prompt_description": t("git_action_all")}, - {"prompt_description": t("git_action_init")}, - {"prompt_description": t("git_action_remote")}, - {"prompt_description": t("git_action_push")}, - {"prompt_description": t("git_action_serve")}, + {"prompt_description": t("Run all (init + remote + push + serve)")}, + {"prompt_description": t("Init - Create bare repos")}, + {"prompt_description": t("Remote - Add local remotes")}, + {"prompt_description": t("Push - Push to local server")}, + {"prompt_description": t("Serve - Start git daemon")}, ] help_info = self.fill_help_info(choices) @@ -785,10 +785,10 @@ class TODO: action="serve", ) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _deploy_git_server(self, production_ready=False, action="all"): - print(t("git_repo_deploy_starting")) + print(t("Starting git server deployment...")) cmd = ( "python3 ./script/git/git_local_server.py -v" f" --action {action}" ) @@ -800,11 +800,11 @@ class TODO: ) def prompt_execute_gpt_code(self): - print(f"🤖 {t('gpt_code_manage')}") + print(f"🤖 {t('AI assistant tools for development!')}") choices = [ - {"prompt_description": t("gpt_code_claude_configs")}, - {"prompt_description": t("gpt_code_claude_add_automation")}, - {"prompt_description": t("menu_rtk")}, + {"prompt_description": t("Configure Claude Code configurations")}, + {"prompt_description": t("Add an automation with Claude in todo.py")}, + {"prompt_description": t("RTK - CLI proxy to reduce LLM token consumption")}, ] help_info = self.fill_help_info(choices) @@ -820,20 +820,20 @@ class TODO: elif status == "3": self.prompt_execute_rtk() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _prompt_claude_configs(self): - print(f"🤖 {t('gpt_code_claude_configs_manage')}") + print(f"🤖 {t('Deploy Claude Code commands!')}") choices = [ - {"prompt_description": t("gpt_code_claude_commit")}, + {"prompt_description": t("Commit - OCA/Odoo commit command")}, { "prompt_description": t( - "gpt_code_claude_todo_add_command" + "Todo Add Command - Add a command to todo.py menu" ) }, { "prompt_description": t( - "gpt_code_claude_list_commands" + "Show installed custom commands" ) }, ] @@ -858,12 +858,12 @@ class TODO: elif status == "3": self._list_claude_commands() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _list_claude_commands(self): commands_dir = os.path.expanduser("~/.claude/commands") if not os.path.isdir(commands_dir): - print(t("gpt_code_claude_no_commands")) + print(t("No custom commands found in ~/.claude/commands/")) return files = sorted( f @@ -871,9 +871,9 @@ class TODO: if f.endswith(".md") ) if not files: - print(t("gpt_code_claude_no_commands")) + print(t("No custom commands found in ~/.claude/commands/")) return - print(t("gpt_code_claude_list_header")) + print(t("Claude Code custom commands:")) print("-" * 50) for f in files: filepath = os.path.join(commands_dir, f) @@ -885,7 +885,7 @@ class TODO: print(f" /{name:<30} {date_str}") print("-" * 50) print( - f"{t('gpt_code_claude_list_total')}" + f"{t('Total:')}" f" {len(files)}" ) @@ -896,12 +896,12 @@ class TODO: dest_file = os.path.join(dest_dir, f"{command_name}.md") if os.path.exists(dest_file): - print(f"{t('gpt_code_cmd_exists')}{dest_file}") + print(f"{t('File already exists: ')}{dest_file}") overwrite = input( - t("gpt_code_cmd_overwrite") + t("Do you want to overwrite the file? (y/Y): ") ).strip() if overwrite not in ("y", "Y"): - print(t("gpt_code_cmd_nothing_to_do")) + print(t("Nothing to do.")) return template_path = os.path.join( @@ -916,8 +916,8 @@ class TODO: content = f.read() if personalize: - name = input(t("gpt_code_enter_name")).strip() - email = input(t("gpt_code_enter_email")).strip() + name = input(t("Enter your full name: ")).strip() + email = input(t("Enter your email: ")).strip() content = content.replace( "Your Name ", f"{name} <{email}>", @@ -931,24 +931,24 @@ class TODO: with open(dest_file, "w") as f: f.write(content) - print(f"{t('gpt_code_cmd_created')}{dest_file}") + print(f"{t('File created successfully: ')}{dest_file}") except Exception as e: - print(f"{t('gpt_code_cmd_error')}{e}") + print(f"{t('Error creating file: ')}{e}") def _claude_add_automation(self): description = input( - t("gpt_code_claude_add_automation_prompt") + t("Description of the command to add: ") ).strip() if not description: return command = input( - t("gpt_code_claude_add_automation_cmd_prompt") + t("Bash command to execute: ") ).strip() if not command: return section = ( input( - t("gpt_code_claude_add_automation_section_prompt") + t("Menu section (git/code/config/network/process): ") ).strip() or "git" ) @@ -970,17 +970,17 @@ class TODO: with open(config_path, "w") as f: json.dump(config, f, indent=4, ensure_ascii=False) f.write("\n") - print(t("gpt_code_claude_add_automation_success")) + print(t("Automation added successfully in todo.json!")) except Exception as e: - print(f"{t('gpt_code_claude_add_automation_error')}{e}") + print(f"{t('Error adding automation: ')}{e}") def prompt_execute_doc(self): - print(f"🤖 {t('doc_search')}") + print(f"🤖 {t('Looking for documentation?')}") choices = [ - {"prompt_description": t("migration_module_coverage")}, - {"prompt_description": t("what_change_between_version")}, - {"prompt_description": t("oca_guidelines")}, - {"prompt_description": t("oca_migration_odoo_19")}, + {"prompt_description": t("Migration module coverage")}, + {"prompt_description": t("What change between version")}, + {"prompt_description": t("OCA guidelines")}, + {"prompt_description": t("OCA migration Odoo 19 milestone")}, ] help_info = self.fill_help_info(choices) @@ -1021,14 +1021,14 @@ class TODO: elif status == "4": print("https://github.com/OCA/maintainer-tools/issues/658") else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_database(self): - print(f"🤖 {t('db_modify')}") + print(f"🤖 {t('Make changes to databases!')}") choices = [ - {"prompt_description": t("download_db_backup")}, - {"prompt_description": t("restore_from_backup")}, - {"prompt_description": t("create_backup")}, + {"prompt_description": t("Download database to create backup (.zip)")}, + {"prompt_description": t("Restore from backup (.zip)")}, + {"prompt_description": t("Create backup (.zip)")}, ] help_info = self.fill_help_info(choices) @@ -1044,13 +1044,13 @@ class TODO: elif status == "3": self.db_manager.create_backup_from_database() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_process(self): - print(f"🤖 {t('process_manage')}") + print(f"🤖 {t('Manage execution processes!')}") choices = [ - {"prompt_description": t("kill_process_port")}, - {"prompt_description": t("kill_git_daemon")}, + {"prompt_description": t("Kill Odoo process from actual port")}, + {"prompt_description": t("Kill git daemon server process")}, ] help_info = self.fill_help_info(choices) @@ -1064,24 +1064,24 @@ class TODO: elif status == "2": self.process_kill_git_daemon() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def process_kill_git_daemon(self): self.execute.exec_command_live( "pkill -f 'git daemon'", source_erplibre=False, ) - print(t("kill_git_daemon_done")) + print(t("Git daemon process killed.")) def prompt_execute_rtk(self): - print(f"🤖 {t('rtk_manage')}") + print(f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}") choices = [ - {"prompt_description": t("rtk_install")}, - {"prompt_description": t("rtk_version")}, - {"prompt_description": t("rtk_gain")}, - {"prompt_description": t("rtk_discover")}, - {"prompt_description": t("rtk_init_global")}, - {"prompt_description": t("rtk_status")}, + {"prompt_description": t("Install RTK")}, + {"prompt_description": t("Check RTK version")}, + {"prompt_description": t("Show cumulative token savings")}, + {"prompt_description": t("Discover optimization opportunities")}, + {"prompt_description": t("Initialize global auto-rewrite hook")}, + {"prompt_description": t("Check RTK status")}, ] help_info = self.fill_help_info(choices) @@ -1103,14 +1103,14 @@ class TODO: elif status == "6": self.rtk_check_status() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def rtk_install(self): - print(f"🤖 {t('rtk_install_method')}") + print(f"🤖 {t('Installation method:')}") choices = [ - {"prompt_description": t("rtk_install_curl")}, - {"prompt_description": t("rtk_install_brew")}, - {"prompt_description": t("rtk_install_cargo")}, + {"prompt_description": t("curl - Automatic install script")}, + {"prompt_description": t("brew - Homebrew (macOS/Linux)")}, + {"prompt_description": t("cargo - Build from source (Rust required)")}, ] help_info = self.fill_help_info(choices) status = click.prompt(help_info) @@ -1133,7 +1133,7 @@ class TODO: source_erplibre=False, ) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def rtk_check_version(self): self.execute.exec_command_live( @@ -1162,7 +1162,7 @@ class TODO: def rtk_check_status(self): rtk_path = shutil.which("rtk") if rtk_path is None: - print(t("rtk_not_installed")) + print(t("RTK is not installed. Use option 1 to install it.")) return result = self.execute.exec_command_live( @@ -1173,24 +1173,24 @@ class TODO: ) if isinstance(result, tuple) and result[0] == 0: version_output = " ".join(result[1]).strip() - print(f"{t('rtk_installed_version')}{version_output}") + print(f"{t('RTK is installed, version: ')}{version_output}") else: - print(f"{t('rtk_installed_version')}?") + print(f"{t('RTK is installed, version: ')}?") config_path = os.path.expanduser("~/.config/rtk/config.toml") if os.path.exists(config_path): - print(t("rtk_hook_active")) + print(t("Global auto-rewrite hook: active")) else: - print(t("rtk_hook_inactive")) + print(t("Global auto-rewrite hook: inactive")) def prompt_execute_config(self): - print(f"🤖 {t('config_manage')}") + print(f"🤖 {t('Manage ERPLibre and Odoo configuration!')}") choices = [ - {"prompt_description": t("generate_all_config")}, - {"prompt_description": t("generate_from_preconfig")}, - {"prompt_description": t("generate_from_backup")}, - {"prompt_description": t("generate_from_database")}, - {"prompt_description": t("setup_queue_job_for_parallelism")}, + {"prompt_description": t("Generate all configuration")}, + {"prompt_description": t("Generate from pre-configuration")}, + {"prompt_description": t("Generate from backup file")}, + {"prompt_description": t("Generate from database")}, + {"prompt_description": t("Setup queue job for parallelism")}, ] help_info = self.fill_help_info(choices) @@ -1210,15 +1210,15 @@ class TODO: elif status == "5": self.generate_config_queue_job() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_network(self): - print(f"🤖 {t('network_tools')}") + print(f"🤖 {t('Network tools!')}") choices = [ - {"prompt_description": t("ssh_port_forwarding")}, + {"prompt_description": t("SSH port-forwarding")}, { "prompt_description": t( - "network_performance_request_per_second" + "Network performance request per second" ) }, ] @@ -1234,7 +1234,7 @@ class TODO: elif status == "2": self.generate_network_performance_test() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def generate_network_port_forwarding(self, add_arg=None): # ssh -L local_port:localhost:remote_port SSH_connection @@ -1261,9 +1261,9 @@ class TODO: ) def prompt_execute_security(self): - print(f"🤖 {t('security_audit')}") + print(f"🤖 {t('Dependency security audit!')}") choices = [ - {"prompt_description": t("pip_audit_desc")}, + {"prompt_description": t("pip-audit - Check vulnerabilities on Python environments")}, ] help_info = self.fill_help_info(choices) @@ -1275,14 +1275,14 @@ class TODO: elif status == "1": self.execute_pip_audit() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_test(self): - print(f"🤖 {t('test_description')}") + print(f"🤖 {t('Test an Odoo module on a temporary database!')}") choices = [ - {"prompt_description": t("test_run_module")}, - {"prompt_description": t("test_run_module_coverage")}, - {"prompt_description": t("test_run_unit_tests")}, + {"prompt_description": t("Test a module")}, + {"prompt_description": t("Test a module with code coverage")}, + {"prompt_description": t("ERPLibre unit tests")}, ] help_info = self.fill_help_info(choices) @@ -1298,25 +1298,25 @@ class TODO: elif status == "3": self.execute_unit_tests() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def execute_test_module(self, coverage=False): # Module name - module_name = input(t("test_enter_module_name")).strip() + module_name = input(t("Module name to test: ")).strip() if not module_name: - print(t("test_module_required")) + print(t("Module name is required!")) return # Database name - db_name = input(t("test_db_name")).strip() + db_name = input(t("Temporary database name (default: test_todo_tmp): ")).strip() if not db_name: db_name = "test_todo_tmp" # Extra modules - extra_modules = input(t("test_install_extra_modules")).strip() + extra_modules = input(t("Extra modules to install (comma-separated, empty for none): ")).strip() # Log level - log_level = input(t("test_log_level")).strip() + log_level = input(t("Log level (default: test): ")).strip() if not log_level: log_level = "test" @@ -1326,7 +1326,7 @@ class TODO: modules_to_install += f",{extra_modules}" # Step 1: Create temp DB - print(f"\n--- {t('test_creating_db')} '{db_name}' ---") + print(f"\n--- {t('Creating temporary database')} '{db_name}' ---") cmd_restore = f"./script/database/db_restore.py --database {db_name}" self.execute.exec_command_live( cmd_restore, @@ -1336,7 +1336,7 @@ class TODO: # Step 2: Install modules print( - f"\n--- {t('test_installing_modules')}: {modules_to_install} ---" + f"\n--- {t('Installing modules')}: {modules_to_install} ---" ) cmd_install = ( f"./script/addons/install_addons.sh" @@ -1349,7 +1349,7 @@ class TODO: ) # Step 3: Run tests - print(f"\n--- {t('test_running')}: {module_name} ---") + print(f"\n--- {t('Running tests')}: {module_name} ---") cmd_test = ( f"ODOO_MODE_TEST=true" f" ./run.sh" @@ -1367,18 +1367,18 @@ class TODO: ) if status_code == 0: - print(f"\n✅ {t('test_success')}") + print(f"\n✅ {t('Tests completed successfully!')}") else: - print(f"\n❌ {t('test_failed')} {status_code}") + print(f"\n❌ {t('Tests failed with return code')} {status_code}") # Step 4: Cleanup lang = get_lang() - keep_input = input(t("test_keep_db")).strip().lower() + keep_input = input(t("Keep the temporary database? (y/N): ")).strip().lower() keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes")) if keep: - print(f"{t('test_db_kept')}: {db_name}") + print(f"{t('Database kept')}: {db_name}") else: - print(f"\n--- {t('test_cleaning_db')} '{db_name}' ---") + print(f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---") cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}" self.execute.exec_command_live( cmd_drop, @@ -1387,7 +1387,7 @@ class TODO: ) def execute_unit_tests(self): - print(f"\n--- {t('test_unit_running')} ---") + print(f"\n--- {t('Running unit tests')} ---") cmd = ( ".venv.erplibre/bin/python -m unittest discover" " -s test -p 'test_*.py' -v" @@ -1398,9 +1398,9 @@ class TODO: return_status_and_output=True, ) if status_code == 0: - print(f"\n✅ {t('test_unit_success')}") + print(f"\n✅ {t('All unit tests passed')}") else: - print(f"\n❌ {t('test_unit_failed')}: {status_code}") + print(f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}") def execute_pip_audit(self): versions, installed_versions, odoo_installed_version = ( @@ -1423,9 +1423,9 @@ class TODO: key_s = str(key_i) label = f"{key_s}: {erplibre_version}" if odoo_version == odoo_installed_version: - label += f" - {t('current')}" - if version_info.get("default"): - label += f" - {t('default')}" + label += f" - {t('Current')}" + if version_info.get("Default"): + label += f" - {t('Default')}" environments[key_s] = { "label": label, @@ -1435,20 +1435,20 @@ class TODO: } if not environments: - print(t("no_env_installed")) + print(t("No installed environment found. Install an Odoo version first.")) return # Show selection menu str_input = ( - f"💬 {t('choose_env_audit')}\n\t" + f"💬 {t('Choose an environment for the audit:')}\n\t" + "\n\t".join([v["label"] for v in environments.values()]) - + f"\n\t0: {t('back')}" - + f"\n{t('selection')}" + + f"\n\t0: {t('Back')}" + + f"\n{t('Select: ')}" ) env_input = "" while env_input not in environments and env_input != "0": if env_input: - print(f"{t('error_value')}" f" '{env_input}'") + print(f"{t('Error, cannot understand value')}" f" '{env_input}'") env_input = input(str_input).strip() if env_input == "0": @@ -1459,12 +1459,12 @@ class TODO: req_path = selected["req_path"] if not os.path.isfile(req_path): - print(f"{t('dep_file_not_found')}{req_path}") + print(f"{t('Dependencies file not found: ')}{req_path}") return # TODO support bash from parameter if open gnome-terminal cmd = f"pip-audit -r {req_path} -l;bash" - print(f"{t('execution')}{cmd}") + print(f"{t('Execution: ')}{cmd}") self.execute.exec_command_live( cmd, source_erplibre=True, @@ -1491,10 +1491,10 @@ class TODO: def generate_config_from_preconfiguration(self): choices = [ - {"prompt_description": t("preconfig_base")}, - {"prompt_description": t("preconfig_base_code_generator")}, - {"prompt_description": t("preconfig_base_image_db")}, - {"prompt_description": t("preconfig_all")}, + {"prompt_description": t("base")}, + {"prompt_description": t("base + code_generator")}, + {"prompt_description": t("base + image_db")}, + {"prompt_description": t("all")}, # {"prompt_description": "base + migration"}, ] help_info = self.fill_help_info(choices) @@ -1523,11 +1523,11 @@ class TODO: # str_group = f"--group {group}" # self.generate_config(add_arg=str_group) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def debug_ide(self): choices = [ - {"prompt_description": t("debug_todo_py")}, + {"prompt_description": t("Debug todo.py")}, ] help_info = self.fill_help_info(choices) @@ -1542,7 +1542,7 @@ class TODO: os.path.join(os.getcwd(), "script/todo/todo.py"), ) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def generate_config_from_backup(self): file_name = self.db_manager.open_file_image_db() @@ -1738,7 +1738,7 @@ class TODO: ) def restart_script(self, last_error): - print(f"🤖 {t('reboot_todo')}") + print(f"🤖 {t('Reboot TODO ...')}") # os.execv(sys.executable, ['python'] + sys.argv) # TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger if os.path.exists(VENV_ERPLIBRE) and not os.path.exists( @@ -1906,13 +1906,13 @@ if __name__ == "__main__": todo.crash_diagnostic(CRASH_E) todo.run() except KeyboardInterrupt: - print(t("keyboard_interrupt")) + print(t("Keyboard interrupt")) finally: end_time = time.time() duration_sec = end_time - start_time if humanize: duration_delta = datetime.timedelta(seconds=duration_sec) humain_time = humanize.precisedelta(duration_delta) - print(f"\n{t('execution_time')} {humain_time}\n") + print(f"\n{t('TODO execution time')} {humain_time}\n") else: - print(f"\n{t('execution_time')} {duration_sec:.2f} sec.\n") + print(f"\n{t('TODO execution time')} {duration_sec:.2f} sec.\n") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index f75c2b5..28c5726 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -15,715 +15,699 @@ TRANSLATIONS = { "fr": "L'importation est un succès!", "en": "Importation success!", }, - "opening": { + "Opening TODO ...": { "fr": "Ouverture de TODO en cours ...", "en": "Opening TODO ...", }, - "enter_directives": { + "=> Enter your choice by number and press Enter!": { "fr": "=> Entre tes directives par son chiffre et fait Entrée!", "en": "=> Enter your choice by number and press Enter!", }, - "command": { + "Command:": { "fr": "Commande :", "en": "Command:", }, - "menu_execute": { + "Execute": { "fr": "Exécution", "en": "Execute", }, - "menu_install": { + "Install": { "fr": "Installation", "en": "Install", }, - "menu_question": { + "Question": { "fr": "Question", "en": "Question", }, - "menu_fork": { + "Fork - Open TODO in a new tab": { "fr": "Fork - Ouvre TODO dans une nouvelle tabulation", "en": "Fork - Open TODO in a new tab", }, - "menu_quit": { + "Quit": { "fr": "Quitter", "en": "Quit", }, - "cmd_not_found": { + "Command not found !": { "fr": "Commande non trouvée !", "en": "Command not found !", }, - "back": { + "Back": { "fr": "Retour", "en": "Back", }, # Execute submenu - "menu_run": { + "Run - Execute and install an instance": { "fr": "Run - Exécuter et installer une instance", "en": "Run - Execute and install an instance", }, - "menu_automation": { + "Automation - Demonstration of developed features": { "fr": "Automatisation - Demonstration des fonctions développées", "en": "Automation - Demonstration of developed features", }, - "menu_update": { + "Update - Update all developed staging source code": { "fr": "Mise à jour - Update all developed staging source code", "en": "Update - Update all developed staging source code", }, - "menu_code": { + "Code - Developer tools": { "fr": "Code - Outil pour développeur", "en": "Code - Developer tools", }, - "menu_doc": { + "Doc - Documentation search": { "fr": "Doc - Recherche de documentation", "en": "Doc - Documentation search", }, - "menu_database": { + "Database - Database tools": { "fr": "Database - Outils sur les bases de données", "en": "Database - Database tools", }, - "menu_process": { + "Process - Execution tools": { "fr": "Process - Outils sur les executions", "en": "Process - Execution tools", }, - "menu_config": { + "Config - Configuration file management": { "fr": "Config - Traitement du fichier de configuration", "en": "Config - Configuration file management", }, - "menu_network": { + "Network - Network tools": { "fr": "Réseau - Outil réseautique", "en": "Network - Network tools", }, - "menu_security": { + "Security - Dependency security audit": { "fr": "Sécurité - Audit de sécurité des dépendances", "en": "Security - Dependency security audit", }, - "menu_rtk": { + "RTK - CLI proxy to reduce LLM token consumption": { "fr": "RTK - Proxy CLI pour réduire la consommation de tokens LLM", "en": "RTK - CLI proxy to reduce LLM token consumption", }, - "menu_lang": { + "Language - Change language / Changer la langue": { "fr": "Langue - Changer la langue / Change language", "en": "Language - Change language / Changer la langue", }, # RTK (Rust Token Killer) - "rtk_manage": { + "Manage RTK (Rust Token Killer) for token optimization!": { "fr": "Gérer RTK (Rust Token Killer) pour optimiser les tokens!", "en": "Manage RTK (Rust Token Killer) for token optimization!", }, - "rtk_install": { + "Install RTK": { "fr": "Installer RTK", "en": "Install RTK", }, - "rtk_version": { + "Check RTK version": { "fr": "Vérifier la version de RTK", "en": "Check RTK version", }, - "rtk_gain": { + "Show cumulative token savings": { "fr": "Afficher les économies de tokens cumulées", "en": "Show cumulative token savings", }, - "rtk_discover": { + "Discover optimization opportunities": { "fr": "Identifier les opportunités d'optimisation", "en": "Discover optimization opportunities", }, - "rtk_init_global": { + "Initialize global auto-rewrite hook": { "fr": "Initialiser le hook auto-rewrite global", "en": "Initialize global auto-rewrite hook", }, - "rtk_status": { + "Check RTK status": { "fr": "Vérifier le statut de RTK", "en": "Check RTK status", }, - "rtk_not_installed": { + "RTK is not installed. Use option 1 to install it.": { "fr": "RTK n'est pas installé. Utilisez l'option 1 pour l'installer.", "en": "RTK is not installed. Use option 1 to install it.", }, - "rtk_installed_version": { + "RTK is installed, version: ": { "fr": "RTK est installé, version : ", "en": "RTK is installed, version: ", }, - "rtk_hook_active": { + "Global auto-rewrite hook: active": { "fr": "Hook auto-rewrite global : actif", "en": "Global auto-rewrite hook: active", }, - "rtk_hook_inactive": { + "Global auto-rewrite hook: inactive": { "fr": "Hook auto-rewrite global : inactif", "en": "Global auto-rewrite hook: inactive", }, - "rtk_install_method": { + "Installation method:": { "fr": "Méthode d'installation :", "en": "Installation method:", }, - "rtk_install_curl": { + "curl - Automatic install script": { "fr": "curl - Script d'installation automatique", "en": "curl - Automatic install script", }, - "rtk_install_brew": { + "brew - Homebrew (macOS/Linux)": { "fr": "brew - Homebrew (macOS/Linux)", "en": "brew - Homebrew (macOS/Linux)", }, - "rtk_install_cargo": { + "cargo - Build from source (Rust required)": { "fr": "cargo - Compilation depuis les sources (Rust requis)", "en": "cargo - Build from source (Rust required)", }, # Prompts and messages - "enter_password": { + "Enter your password: ": { "fr": "Entrez votre mot de passe : ", "en": "Enter your password: ", }, - "ia_prompt": { + "Write your question ": { "fr": "Écrit moi ta question ", "en": "Write your question ", }, - "new_instance_confirm": { + "Do you want a new instance?": { "fr": "Voulez-vous une nouvelle instance?", "en": "Do you want a new instance?", }, - "ssh_port_forwarding": { + "SSH port-forwarding": { "fr": "SSH port-forwarding", "en": "SSH port-forwarding", }, - "network_performance_request_per_second": { + "Network performance request per second": { "fr": "Performance réseau en requêtes par seconde", "en": "Network performance request per second", }, - "setup_queue_job_for_parallelism": { + "Setup queue job for parallelism": { "fr": "Configurer la file d'attente pour l'exécution parallèle", "en": "Setup queue job for parallelism", }, - "choose_database": { + "Choose your database": { "fr": "Choisir sa base de données", "en": "Choose your database", }, - "update_dev": { + "Development update": { "fr": "Mise à jour du développement", "en": "Development update", }, - "code_need": { + "What do you need for development?": { "fr": "Qu'avez-vous de besoin pour développer?", "en": "What do you need for development?", }, - "doc_search": { + "Looking for documentation?": { "fr": "Vous cherchez de la documentation?", "en": "Looking for documentation?", }, - "db_modify": { + "Make changes to databases!": { "fr": "Faites des modifications sur les bases de données!", "en": "Make changes to databases!", }, - "process_manage": { + "Manage execution processes!": { "fr": "Manipuler les processus d'exécution!", "en": "Manage execution processes!", }, - "config_manage": { + "Manage ERPLibre and Odoo configuration!": { "fr": "Manipuler la configuration ERPLibre et Odoo!", "en": "Manage ERPLibre and Odoo configuration!", }, - "network_tools": { + "Network tools!": { "fr": "Outil réseautique!", "en": "Network tools!", }, - "security_audit": { + "Dependency security audit!": { "fr": "Audit de securite des dépendances!", "en": "Dependency security audit!", }, - "script_failed": { + "The Bash script failed with return code": { "fr": "Le script Bash a échoué avec le code de retour", "en": "The Bash script failed with return code", }, - "no_env_installed": { + "No installed environment found. Install an Odoo version first.": { "fr": "Aucun environnement installe trouve. Installez d'abord une version d'Odoo.", "en": "No installed environment found. Install an Odoo version first.", }, - "choose_env_audit": { + "Choose an environment for the audit:": { "fr": "Choisir un environnement pour l'audit :", "en": "Choose an environment for the audit:", }, - "selection": { + "Select: ": { "fr": "Sélection : ", "en": "Select: ", }, - "error_value": { + "Error, cannot understand value": { "fr": "Erreur, impossible de comprendre la valeur", "en": "Error, cannot understand value", }, - "dep_file_not_found": { + "Dependencies file not found: ": { "fr": "Fichier de dépendances introuvable : ", "en": "Dependencies file not found: ", }, - "execution": { + "Execution: ": { "fr": "Execution : ", "en": "Execution: ", }, - "current": { + "Current": { "fr": "Actuel", "en": "Current", }, - "default": { + "Default": { "fr": "Défaut", "en": "Default", }, - "reboot_todo": { + "Reboot TODO ...": { "fr": "Reboot TODO ...", "en": "Reboot TODO ...", }, - "pip_audit_desc": { + "pip-audit - Check vulnerabilities on Python environments": { "fr": "pip-audit - Verifier les vulnérabilités sur les environnements Python", "en": "pip-audit - Check vulnerabilities on Python environments", }, - "will_execute": { + "Will execute:": { "fr": "Va exécuter :", "en": "Will execute:", }, - "choose_version": { + "Choose a version:": { "fr": "Choisir une version :", "en": "Choose a version:", }, - "error_cannot_understand": { - "fr": "Erreur, impossible de comprendre la valeur", - "en": "Error, cannot understand value", - }, # todo.json translatable prompt_descriptions - "json_instance_test": { + "Test - Minimal base instance": { "fr": "Test - Instance de base minimale", "en": "Test - Minimal base instance", }, - "json_robot_minimal": { + "Open RobotLibre 🤖 minimal": { "fr": "Ouvrir RobotLibre 🤖 minimal", "en": "Open RobotLibre 🤖 minimal", }, - "json_robot_search": { + "Open RobotLibre 🤖 with search enabled": { "fr": "Ouvrir RobotLibre 🤖 en activant la recherche", "en": "Open RobotLibre 🤖 with search enabled", }, - "json_open_erplibre_todo": { + "Open ERPLibre with TODO 🤖": { "fr": "Ouvrir ERPLibre avec TODO 🤖", "en": "Open ERPLibre with TODO 🤖", }, - "json_update_erplibre_base_test": { + "Update all erplibre_base on database test": { "fr": "Mise à jour de tous les erplibre_base sur la base de données test", "en": "Update all erplibre_base on database test", }, - "json_show_code_status": { + "Show code status": { "fr": "Afficher le statut du code", "en": "Show code status", }, - "json_stash_all_code": { + "Stash all code": { "fr": "Remiser tout le code", "en": "Stash all code", }, - "json_format_modified_code": { + "Format modified code": { "fr": "Formater le code modifié", "en": "Format modified code", }, # todo.py hardcoded prompt_descriptions - "mobile_compile_run": { + "Mobile - Compile and run software": { "fr": "Mobile - Compiler et exécuter le logiciel", "en": "Mobile - Compile and run software", }, - "upgrade_odoo_migration": { + "Upgrade Odoo - Migration Database": { "fr": "Mise à jour Odoo - Migration de base de données", "en": "Upgrade Odoo - Migration Database", }, - "upgrade_poetry_dependency": { + "Upgrade Poetry - Dependency of Odoo": { "fr": "Mise à jour Poetry - Dépendances d'Odoo", "en": "Upgrade Poetry - Dependency of Odoo", }, - "open_shell": { + "Open SHELL": { "fr": "Ouvrir le SHELL", "en": "Open SHELL", }, - "upgrade_module": { + "Upgrade Module": { "fr": "Mise à jour de module", "en": "Upgrade Module", }, - "debug": { + "Debug": { "fr": "Débogage", "en": "Debug", }, - "migration_module_coverage": { + "Migration module coverage": { "fr": "Couverture de migration des modules", "en": "Migration module coverage", }, - "what_change_between_version": { + "What change between version": { "fr": "Quels changements entre les versions", "en": "What change between version", }, - "oca_guidelines": { + "OCA guidelines": { "fr": "Directives OCA", "en": "OCA guidelines", }, - "oca_migration_odoo_19": { + "OCA migration Odoo 19 milestone": { "fr": "Migration OCA Odoo 19 - Jalons", "en": "OCA migration Odoo 19 milestone", }, - "download_db_backup": { + "Download database to create backup (.zip)": { "fr": "Télécharger une base de données pour créer une sauvegarde (.zip)", "en": "Download database to create backup (.zip)", }, - "restore_from_backup": { + "Restore from backup (.zip)": { "fr": "Restaurer a partir d'une sauvegarde (.zip)", "en": "Restore from backup (.zip)", }, - "create_backup": { + "Create backup (.zip)": { "fr": "Créer une sauvegarde (.zip)", "en": "Create backup (.zip)", }, - "kill_process_port": { + "Kill Odoo process from actual port": { "fr": "Terminer le processus Odoo du port actuel", "en": "Kill Odoo process from actual port", }, - "kill_git_daemon": { + "Kill git daemon server process": { "fr": "Terminer le processus du serveur git daemon", "en": "Kill git daemon server process", }, - "kill_git_daemon_done": { + "Git daemon process killed.": { "fr": "Processus git daemon terminé.", "en": "Git daemon process killed.", }, - "generate_all_config": { + "Generate all configuration": { "fr": "Générer toute la configuration", "en": "Generate all configuration", }, - "generate_from_preconfig": { + "Generate from pre-configuration": { "fr": "Générer a partir de la pre-configuration", "en": "Generate from pre-configuration", }, - "generate_from_backup": { + "Generate from backup file": { "fr": "Générer a partir d'un fichier de sauvegarde", "en": "Generate from backup file", }, - "generate_from_database": { + "Generate from database": { "fr": "Générer a partir de la base de données", "en": "Generate from database", }, - "preconfig_base": { + "base": { "fr": "base", "en": "base", }, - "preconfig_base_code_generator": { + "base + code_generator": { "fr": "base + code_generator", "en": "base + code_generator", }, - "preconfig_base_image_db": { + "base + image_db": { "fr": "base + image_db", "en": "base + image_db", }, - "preconfig_all": { + "all": { "fr": "tout", "en": "all", }, - "debug_todo_py": { + "Debug todo.py": { "fr": "Débogage todo.py", "en": "Debug todo.py", }, # Test section - "menu_test": { + "Test - Test an Odoo module": { "fr": "Test - Tester un module Odoo", "en": "Test - Test an Odoo module", }, - "test_description": { + "Test an Odoo module on a temporary database!": { "fr": "Tester un module Odoo sur une base de données temporaire!", "en": "Test an Odoo module on a temporary database!", }, - "test_run_module": { + "Test a module": { "fr": "Tester un module", "en": "Test a module", }, - "test_run_module_coverage": { + "Test a module with code coverage": { "fr": "Tester un module avec couverture de code", "en": "Test a module with code coverage", }, - "test_run_unit_tests": { + "ERPLibre unit tests": { "fr": "Tests unitaires ERPLibre", "en": "ERPLibre unit tests", }, - "test_unit_running": { + "Running unit tests": { "fr": "Exécution des tests unitaires", "en": "Running unit tests", }, - "test_unit_success": { + "All unit tests passed": { "fr": "Tous les tests unitaires ont réussi", "en": "All unit tests passed", }, - "test_unit_failed": { + "Some unit tests failed, exit code": { "fr": "Des tests unitaires ont échoué, code de sortie", "en": "Some unit tests failed, exit code", }, - "test_enter_module_name": { + "Module name to test: ": { "fr": "Nom du module à tester : ", "en": "Module name to test: ", }, - "test_db_name": { + "Temporary database name (default: test_todo_tmp): ": { "fr": "Nom de la base de données temporaire (défaut: test_todo_tmp) : ", "en": "Temporary database name (default: test_todo_tmp): ", }, - "test_install_extra_modules": { + "Extra modules to install (comma-separated, empty for none): ": { "fr": "Modules supplémentaires à installer (séparés par des virgules, vide pour aucun) : ", "en": "Extra modules to install (comma-separated, empty for none): ", }, - "test_log_level": { + "Log level (default: test): ": { "fr": "Niveau de log (défaut: test) : ", "en": "Log level (default: test): ", }, - "test_creating_db": { + "Creating temporary database": { "fr": "Création de la base de données temporaire", "en": "Creating temporary database", }, - "test_installing_modules": { + "Installing modules": { "fr": "Installation des modules", "en": "Installing modules", }, - "test_running": { + "Running tests": { "fr": "Exécution des tests", "en": "Running tests", }, - "test_cleaning_db": { + "Cleaning up temporary database": { "fr": "Suppression de la base de données temporaire", "en": "Cleaning up temporary database", }, - "test_keep_db": { + "Keep the temporary database? (y/N): ": { "fr": "Conserver la base de données temporaire? (o/N) : ", "en": "Keep the temporary database? (y/N): ", }, - "test_db_kept": { + "Database kept": { "fr": "Base de données conservée", "en": "Database kept", }, - "test_success": { + "Tests completed successfully!": { "fr": "Tests terminés avec succès!", "en": "Tests completed successfully!", }, - "test_failed": { + "Tests failed with return code": { "fr": "Les tests ont échoué avec le code de retour", "en": "Tests failed with return code", }, - "test_module_required": { + "Module name is required!": { "fr": "Le nom du module est requis!", "en": "Module name is required!", }, # Git section - "menu_git": { + "Git - Git tools": { "fr": "Git - Outils Git", "en": "Git - Git tools", }, - "git_manage": { + "Git management tools!": { "fr": "Outils de gestion Git!", "en": "Git management tools!", }, - "git_local_server": { + "Local git server": { "fr": "Serveur git local", "en": "Local git server", }, - "git_repo_manage": { + "Manage local git repository server!": { "fr": "Gérer le serveur de dépôts git local!", "en": "Manage local git repository server!", }, - "git_repo_deploy_local": { + "Deploy a local git server (~/.git-server)": { "fr": "Déployer un serveur git local (~/.git-server)", "en": "Deploy a local git server (~/.git-server)", }, - "git_repo_deploy_production": { + "Deploy a production git server (/srv/git, root required)": { "fr": "Déployer un serveur git production (/srv/git, root requis)", "en": "Deploy a production git server (/srv/git, root required)", }, - "git_repo_deploy_starting": { + "Starting git server deployment...": { "fr": "Démarrage du déploiement du serveur git...", "en": "Starting git server deployment...", }, - "git_mode_local": { + "Local mode (~/.git-server)": { "fr": "Mode local (~/.git-server)", "en": "Local mode (~/.git-server)", }, - "git_mode_production": { + "Production mode (/srv/git, root required)": { "fr": "Mode production (/srv/git, root requis)", "en": "Production mode (/srv/git, root required)", }, - "git_action_all": { + "Run all (init + remote + push + serve)": { "fr": "Tout exécuter (init + remote + push + serve)", "en": "Run all (init + remote + push + serve)", }, - "git_action_init": { + "Init - Create bare repos": { "fr": "Init - Créer les bare repos", "en": "Init - Create bare repos", }, - "git_action_remote": { + "Remote - Add local remotes": { "fr": "Remote - Ajouter les remotes locaux", "en": "Remote - Add local remotes", }, - "git_action_push": { + "Push - Push to local server": { "fr": "Push - Pousser vers le serveur local", "en": "Push - Push to local server", }, - "git_action_serve": { + "Serve - Start git daemon": { "fr": "Serve - Démarrer le daemon git", "en": "Serve - Start git daemon", }, # Git remote add - "git_add_remote": { + "Add a remote to a local repository": { "fr": "Ajouter un remote vers un dépôt local", "en": "Add a remote to a local repository", }, - "git_add_remote_name_prompt": { + "Remote name (default: localhost): ": { "fr": "Nom du remote (défaut: localhost) : ", "en": "Remote name (default: localhost): ", }, - "git_add_remote_url_prompt": { + "Repository address (e.g.: git://192.168.1.100/my-repo.git): ": { "fr": "Adresse du dépôt (ex: git://192.168.1.100/mon-repo.git) : ", "en": "Repository address (e.g.: git://192.168.1.100/my-repo.git): ", }, - "git_add_remote_url_required": { + "Repository address is required!": { "fr": "L'adresse du dépôt est requise!", "en": "Repository address is required!", }, - "git_add_remote_success": { + "Remote added successfully!": { "fr": "Remote ajouté avec succès!", "en": "Remote added successfully!", }, - "git_add_remote_error": { + "Error adding remote: ": { "fr": "Erreur lors de l'ajout du remote : ", "en": "Error adding remote: ", }, # Git config vim - "git_config_vim": { + "Configure git local editor to vim": { "fr": "Configuration git local par vim", "en": "Configure git local editor to vim", }, - "git_config_vim_success": { + "Git editor configured to vim successfully!": { "fr": "Éditeur git configuré sur vim avec succès!", "en": "Git editor configured to vim successfully!", }, - "git_config_vim_error": { + "Error during configuration: ": { "fr": "Erreur lors de la configuration : ", "en": "Error during configuration: ", }, # GPT code - Claude automation - "gpt_code_claude_add_automation": { + "Add an automation with Claude in todo.py": { "fr": "Ajouter une automatisation avec Claude dans todo.py", "en": "Add an automation with Claude in todo.py", }, - "gpt_code_claude_add_automation_prompt": { + "Description of the command to add: ": { "fr": "Description de la commande à ajouter : ", "en": "Description of the command to add: ", }, - "gpt_code_claude_add_automation_cmd_prompt": { + "Bash command to execute: ": { "fr": "Commande bash à exécuter : ", "en": "Bash command to execute: ", }, - "gpt_code_claude_add_automation_section_prompt": { + "Menu section (git/code/config/network/process): ": { "fr": "Section du menu (git/code/config/network/process) : ", "en": "Menu section (git/code/config/network/process): ", }, - "gpt_code_claude_add_automation_success": { + "Automation added successfully in todo.json!": { "fr": "Automatisation ajoutée avec succès dans todo.json!", "en": "Automation added successfully in todo.json!", }, - "gpt_code_claude_add_automation_error": { + "Error adding automation: ": { "fr": "Erreur lors de l'ajout de l'automatisation : ", "en": "Error adding automation: ", }, # Language selection - "lang_prompt": { + "Choose language / Choisir la langue": { "fr": "Choisir la langue / Choose language", "en": "Choose language / Choisir la langue", }, - "lang_french": { + "French": { "fr": "Francais", "en": "French", }, - "lang_english": { + "English": { "fr": "Anglais", "en": "English", }, - "lang_changed": { + "Language changed to: English": { "fr": "Langue changée pour : Francais", "en": "Language changed to: English", }, - "execution_time": { + "TODO execution time": { "fr": "Temps d'exécution TODO", "en": "TODO execution time", }, - "keyboard_interrupt": { + "Keyboard interrupt": { "fr": "Interruption clavier", "en": "Keyboard interrupt", }, # GPT code section - "menu_gpt_code": { + "GPT code - AI assistant tools": { "fr": "GPT code - Outils d'assistant IA", "en": "GPT code - AI assistant tools", }, - "gpt_code_manage": { + "AI assistant tools for development!": { "fr": "Outils d'assistant IA pour le développement!", "en": "AI assistant tools for development!", }, - "gpt_code_claude_configs": { + "Configure Claude Code configurations": { "fr": "Configurer les configurations Claude Code", "en": "Configure Claude Code configurations", }, - "gpt_code_claude_commit": { + "Commit - OCA/Odoo commit command": { "fr": "Commit - Commande de commit OCA/Odoo", "en": "Commit - OCA/Odoo commit command", }, - "gpt_code_claude_todo_add_command": { + "Todo Add Command - Add a command to todo.py menu": { "fr": "Todo Add Command - Ajouter une commande au menu todo.py", "en": "Todo Add Command - Add a command to todo.py menu", }, - "gpt_code_enter_name": { + "Enter your full name: ": { "fr": "Entrez votre nom complet : ", "en": "Enter your full name: ", }, - "gpt_code_enter_email": { + "Enter your email: ": { "fr": "Entrez votre courriel : ", "en": "Enter your email: ", }, - "gpt_code_claude_configs_manage": { + "Deploy Claude Code commands!": { "fr": "Déployer les commandes Claude Code!", "en": "Deploy Claude Code commands!", }, - "gpt_code_claude_list_commands": { + "Show installed custom commands": { "fr": "Afficher les commandes personnalisées installées", "en": "Show installed custom commands", }, - "gpt_code_claude_no_commands": { + "No custom commands found in ~/.claude/commands/": { "fr": "Aucune commande personnalisée trouvée dans ~/.claude/commands/", "en": "No custom commands found in ~/.claude/commands/", }, - "gpt_code_claude_list_header": { + "Claude Code custom commands:": { "fr": "Commandes personnalisées Claude Code :", "en": "Claude Code custom commands:", }, - "gpt_code_claude_list_total": { + "Total:": { "fr": "Total :", "en": "Total:", }, - "gpt_code_cmd_exists": { + "File already exists: ": { "fr": "Le fichier existe déjà : ", "en": "File already exists: ", }, - "gpt_code_cmd_overwrite": { + "Do you want to overwrite the file? (y/Y): ": { "fr": "Voulez-vous écraser le fichier? (y/Y) : ", "en": "Do you want to overwrite the file? (y/Y): ", }, - "gpt_code_cmd_nothing_to_do": { + "Nothing to do.": { "fr": "Rien à faire.", "en": "Nothing to do.", }, - "gpt_code_cmd_created": { + "File created successfully: ": { "fr": "Fichier créé avec succès : ", "en": "File created successfully: ", }, - "gpt_code_cmd_error": { - "fr": "Erreur lors de la création du fichier : ", - "en": "Error creating file: ", - }, - "gpt_code_commit_exists": { - "fr": "Le fichier ~/.claude/commands/commit.md existe déjà. Aucune action effectuée.", - "en": "File ~/.claude/commands/commit.md already exists. No action taken.", - }, - "gpt_code_commit_created": { - "fr": "Fichier ~/.claude/commands/commit.md créé avec succès!", - "en": "File ~/.claude/commands/commit.md created successfully!", - }, - "gpt_code_commit_error": { + "Error creating file: ": { "fr": "Erreur lors de la création du fichier : ", "en": "Error creating file: ", }, From bee6d50018422a244d45d03b3fed8545412847ba Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 05:40:22 -0400 Subject: [PATCH 8/9] [ADD] todo: add deploy menu with git clone Allow users to deploy ERPLibre to a local directory via git clone from the TODO interactive menu, making onboarding simpler for new developers. Generated by Claude Code 2.1.74 model claude-opus-4-6 Co-Authored-By: Mathieu Benoit --- script/todo/todo.py | 60 ++++++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 33 ++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/script/todo/todo.py b/script/todo/todo.py index d9a9790..5fdfedd 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -208,6 +208,7 @@ class TODO: [12] {t("Security - Dependency security audit")} [13] {t("Test - Test an Odoo module")} [14] {t("Update - Update all developed staging source code")} +[15] {t("Deploy - Deploy ERPLibre locally")} [0] {t("Back")} """ while True: @@ -271,6 +272,10 @@ class TODO: status = self.prompt_execute_update() if status is not False: return + elif status == "15": + status = self.prompt_execute_deploy() + if status is not False: + return else: print(t("Command not found !")) @@ -606,6 +611,61 @@ class TODO: if cmd_no_found: print(t("Command not found !")) + def prompt_execute_deploy(self): + print( + f"🤖 {t('Deploy ERPLibre to a local directory!')}" + ) + choices = [ + { + "prompt_description": t( + "Clone ERPLibre locally (git clone)" + ) + }, + ] + help_info = self.fill_help_info(choices) + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + elif status == "1": + self._deploy_clone_erplibre() + else: + print(t("Command not found !")) + + def _deploy_clone_erplibre(self): + default_path = os.path.expanduser("~/erplibre") + target_path = ( + input( + t("Target directory path (default: ~/erplibre): ") + ).strip() + or default_path + ) + target_path = os.path.expanduser(target_path) + if os.path.exists(target_path): + print( + f"{t('Directory already exists: ')}{target_path}" + ) + return + print(t("Cloning ERPLibre...")) + cmd = ( + "git clone" + " https://github.com/erplibre/erplibre" + f" {target_path}" + ) + print(f"{t('Will execute:')} {cmd}") + try: + self.execute.exec_command_live( + cmd, source_erplibre=False + ) + print( + f"{t('ERPLibre cloned successfully to: ')}" + f"{target_path}" + ) + except Exception as e: + print(f"{t('Error cloning ERPLibre: ')}{e}") + def prompt_execute_code(self): print(f"🤖 {t('What do you need for development?')}") # help_info = """Commande : diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 28c5726..1e9846e 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -104,6 +104,39 @@ TRANSLATIONS = { "fr": "Langue - Changer la langue / Change language", "en": "Language - Change language / Changer la langue", }, + # Deploy section + "Deploy - Deploy ERPLibre locally": { + "fr": "Déploiement - Déployer ERPLibre localement", + "en": "Deploy - Deploy ERPLibre locally", + }, + "Deploy ERPLibre to a local directory!": { + "fr": "Déployer ERPLibre dans un répertoire local!", + "en": "Deploy ERPLibre to a local directory!", + }, + "Clone ERPLibre locally (git clone)": { + "fr": "Cloner ERPLibre localement (git clone)", + "en": "Clone ERPLibre locally (git clone)", + }, + "Target directory path (default: ~/erplibre): ": { + "fr": "Chemin du répertoire cible (défaut: ~/erplibre) : ", + "en": "Target directory path (default: ~/erplibre): ", + }, + "Directory already exists: ": { + "fr": "Le répertoire existe déjà : ", + "en": "Directory already exists: ", + }, + "Cloning ERPLibre...": { + "fr": "Clonage d'ERPLibre en cours...", + "en": "Cloning ERPLibre...", + }, + "ERPLibre cloned successfully to: ": { + "fr": "ERPLibre cloné avec succès dans : ", + "en": "ERPLibre cloned successfully to: ", + }, + "Error cloning ERPLibre: ": { + "fr": "Erreur lors du clonage d'ERPLibre : ", + "en": "Error cloning ERPLibre: ", + }, # RTK (Rust Token Killer) "Manage RTK (Rust Token Killer) for token optimization!": { "fr": "Gérer RTK (Rust Token Killer) pour optimiser les tokens!", From 36ae1d9beb5ece755daa464154e4884523b4a1a8 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 13 Mar 2026 15:04:14 -0400 Subject: [PATCH 9/9] [UPD] script Format --- .../code/git_commit_migration_addons_path.py | 4 +- script/database/list_remote.py | 21 ++- script/git/git_local_server.py | 120 +++---------- script/git/github_api.py | 64 +++---- script/ide/pycharm_configuration.py | 4 +- script/odoo/util/show_installed_module.py | 4 +- ...7_to_postgresql_18_module_mail_nov_2025.py | 14 +- script/todo/todo.py | 167 ++++++++++-------- 8 files changed, 171 insertions(+), 227 deletions(-) diff --git a/script/code/git_commit_migration_addons_path.py b/script/code/git_commit_migration_addons_path.py index a725200..761a308 100755 --- a/script/code/git_commit_migration_addons_path.py +++ b/script/code/git_commit_migration_addons_path.py @@ -96,9 +96,7 @@ def commit_by_directory(): run_git_command(f'cd "{config.path}" && git rm -r {directory}') # Crée le commit - commit_message = ( - f"[{mig_prefix_msg}] {directory}: Migration to {config.odoo_version}" - ) + commit_message = f"[{mig_prefix_msg}] {directory}: Migration to {config.odoo_version}" commit_result = run_git_command( f'cd "{config.path}" && git commit -m "{commit_message}"' ) diff --git a/script/database/list_remote.py b/script/database/list_remote.py index d224c30..9c1e22a 100755 --- a/script/database/list_remote.py +++ b/script/database/list_remote.py @@ -10,28 +10,31 @@ def get_db_list_xmlrpc(odoo_url): Retrieves the list of Odoo databases using the XML-RPC API. """ try: - common = xmlrpc.client.ServerProxy(f'{odoo_url}/xmlrpc/db') + common = xmlrpc.client.ServerProxy(f"{odoo_url}/xmlrpc/db") db_list = common.list() return db_list except xmlrpc.client.Fault as e: - print(f"XML-RPC Error: {e.faultCode} - {e.faultString}", file=sys.stderr) + print( + f"XML-RPC Error: {e.faultCode} - {e.faultString}", file=sys.stderr + ) return [] except Exception as e: print(f"Connection Error: {e}", file=sys.stderr) return [] + # --- CLI using Click --- @click.command() @click.option( - '--odoo-url', - default='http://localhost:8069', - help='URL of the Odoo server.', - show_default=True + "--odoo-url", + default="http://localhost:8069", + help="URL of the Odoo server.", + show_default=True, ) @click.option( - '--raw', + "--raw", is_flag=True, - help='Output one database per line, without extra formatting. Useful for scripting.' + help="Output one database per line, without extra formatting. Useful for scripting.", ) def list_databases(odoo_url, raw=False): """ @@ -56,5 +59,5 @@ def list_databases(odoo_url, raw=False): # --- Script Execution --- -if __name__ == '__main__': +if __name__ == "__main__": list_databases() diff --git a/script/git/git_local_server.py b/script/git/git_local_server.py index 7fcc584..3f1803b 100755 --- a/script/git/git_local_server.py +++ b/script/git/git_local_server.py @@ -151,8 +151,7 @@ Use --production-ready for /srv/git (requires root). type=int, default=DEFAULT_JOBS, help=( - "Parallel jobs for init/remote/push" - f" (default: {DEFAULT_JOBS})" + "Parallel jobs for init/remote/push" f" (default: {DEFAULT_JOBS})" ), ) parser.add_argument( @@ -247,11 +246,7 @@ def get_erplibre_root_project(erplibre_root): capture_output=True, text=True, ) - revision = ( - result.stdout.strip() - if result.returncode == 0 - else "master" - ) + revision = result.stdout.strip() if result.returncode == 0 else "master" return { "name": ERPLIBRE_REPO_NAME, @@ -278,19 +273,13 @@ async def _init_single_bare_repo(git_path, project, semaphore): return "skipped" _logger.info(f" Creating: {bare_path}") - _, err, rc = await _run_git( - "git", "init", "--bare", bare_path - ) + _, err, rc = await _run_git("git", "init", "--bare", bare_path) if rc != 0: - _logger.warning( - f" Init failed: {bare_path}: {err.strip()}" - ) + _logger.warning(f" Init failed: {bare_path}: {err.strip()}") return "error" # Enable git daemon export - export_file = os.path.join( - bare_path, "git-daemon-export-ok" - ) + export_file = os.path.join(bare_path, "git-daemon-export-ok") open(export_file, "w").close() # Allow pushing from shallow clones @@ -312,10 +301,7 @@ async def init_bare_repos(git_path, projects, jobs): semaphore = asyncio.Semaphore(jobs) results = await asyncio.gather( - *[ - _init_single_bare_repo(git_path, p, semaphore) - for p in projects - ] + *[_init_single_bare_repo(git_path, p, semaphore) for p in projects] ) created = results.count("created") @@ -352,9 +338,7 @@ async def _add_single_remote( ): """Add or update remote for a single repo (async).""" async with semaphore: - repo_path = os.path.join( - erplibre_root, project["path"] - ) + repo_path = os.path.join(erplibre_root, project["path"]) if not os.path.isdir(repo_path): _logger.warning(f" Not found: {repo_path}") return "error" @@ -368,9 +352,7 @@ async def _add_single_remote( ) # Check if remote already exists - stdout, _, _ = await _run_git( - "git", "-C", repo_path, "remote" - ) + stdout, _, _ = await _run_git("git", "-C", repo_path, "remote") existing_remotes = stdout.strip().split("\n") if remote_name in existing_remotes: @@ -403,8 +385,7 @@ async def _add_single_remote( ) if rc != 0: _logger.warning( - f" add failed for" - f" {project['path']}: {err.strip()}" + f" add failed for" f" {project['path']}: {err.strip()}" ) return "error" _logger.info(f" Added: {project['path']}") @@ -447,10 +428,7 @@ async def add_remotes( added = results.count("added") updated = results.count("updated") errors = results.count("error") - print( - f"Remotes: {added} added, {updated} updated," - f" {errors} errors" - ) + print(f"Remotes: {added} added, {updated} updated," f" {errors} errors") # --- Async workers for push --- @@ -458,9 +436,7 @@ async def add_remotes( async def _is_detached_head(repo_path): """Check if a repo is in detached HEAD state.""" - _, _, rc = await _run_git( - "git", "-C", repo_path, "symbolic-ref", "HEAD" - ) + _, _, rc = await _run_git("git", "-C", repo_path, "symbolic-ref", "HEAD") return rc != 0 @@ -471,9 +447,7 @@ async def _checkout_manifest_branch(repo_path, revision): on the exact commit. We create/checkout a local branch matching the manifest revision so git push works. """ - branch = ( - revision.split("/")[-1] if "/" in revision else revision - ) + branch = revision.split("/")[-1] if "/" in revision else revision # Check if local branch already exists _, _, rc = await _run_git( @@ -485,9 +459,7 @@ async def _checkout_manifest_branch(repo_path, revision): f"refs/heads/{branch}", ) if rc == 0: - await _run_git( - "git", "-C", repo_path, "checkout", branch - ) + await _run_git("git", "-C", repo_path, "checkout", branch) else: await _run_git( "git", @@ -514,9 +486,7 @@ async def _update_bare_head(git_path, project): revision = project.get("revision", "") if not revision: return - branch = ( - revision.split("/")[-1] if "/" in revision else revision - ) + branch = revision.split("/")[-1] if "/" in revision else revision await _run_git( "git", "-C", @@ -529,20 +499,10 @@ async def _update_bare_head(git_path, project): async def _try_unshallow(repo_path, project, remote_name): """Try to unshallow a repo by fetching full history.""" - shallow_file = os.path.join( - repo_path, ".git", "shallow" - ) - _logger.info( - f" Unshallowing {project['path']}..." - ) - stdout, _, _ = await _run_git( - "git", "-C", repo_path, "remote" - ) - remotes = [ - r - for r in stdout.strip().split("\n") - if r and r != remote_name - ] + shallow_file = os.path.join(repo_path, ".git", "shallow") + _logger.info(f" Unshallowing {project['path']}...") + stdout, _, _ = await _run_git("git", "-C", repo_path, "remote") + remotes = [r for r in stdout.strip().split("\n") if r and r != remote_name] manifest_remote = project.get("remote", "") if manifest_remote in remotes: remotes.remove(manifest_remote) @@ -562,9 +522,7 @@ async def _try_unshallow(repo_path, project, remote_name): except asyncio.TimeoutError: continue if not os.path.exists(shallow_file): - _logger.info( - f" Unshallowed via {try_remote}" - ) + _logger.info(f" Unshallowed via {try_remote}") return if os.path.exists(shallow_file): _logger.warning( @@ -585,30 +543,22 @@ async def _push_single_repo( ): """Push a single repo to local remote (async).""" async with semaphore: - repo_path = os.path.join( - erplibre_root, project["path"] - ) + repo_path = os.path.join(erplibre_root, project["path"]) if not os.path.isdir(repo_path): _logger.warning(f" Not found: {repo_path}") return "error", False # Handle shallow repos - shallow_file = os.path.join( - repo_path, ".git", "shallow" - ) + shallow_file = os.path.join(repo_path, ".git", "shallow") if os.path.exists(shallow_file): if unshallow: - await _try_unshallow( - repo_path, project, remote_name - ) + await _try_unshallow(repo_path, project, remote_name) else: # Enable shallow push on bare repo repo_name = project["name"] if not repo_name.endswith(".git"): repo_name += ".git" - bare_path = os.path.join( - git_path, repo_name - ) + bare_path = os.path.join(git_path, repo_name) if os.path.isdir(bare_path): await _run_git( "git", @@ -618,19 +568,14 @@ async def _push_single_repo( "receive.shallowUpdate", "true", ) - _logger.info( - f" Shallow push for" - f" {project['path']}" - ) + _logger.info(f" Shallow push for" f" {project['path']}") # Handle detached HEAD: checkout manifest branch did_checkout = False if await _is_detached_head(repo_path): revision = project.get("revision", "") if revision: - branch = await _checkout_manifest_branch( - repo_path, revision - ) + branch = await _checkout_manifest_branch(repo_path, revision) _logger.info( f" Checkout {branch} for" f" {project['path']}" @@ -663,9 +608,7 @@ async def _push_single_repo( "push", remote_name, ] - _, err, rc = await _run_git( - *cmd, timeout=120 - ) + _, err, rc = await _run_git(*cmd, timeout=120) if rc != 0: _logger.warning( f" Push failed for" @@ -675,14 +618,10 @@ async def _push_single_repo( return "error", did_checkout else: await _update_bare_head(git_path, project) - _logger.info( - f" Pushed: {project['path']}" - ) + _logger.info(f" Pushed: {project['path']}") return "pushed", did_checkout except asyncio.TimeoutError: - _logger.warning( - f" Timeout pushing {project['path']}" - ) + _logger.warning(f" Timeout pushing {project['path']}") return "error", did_checkout @@ -744,8 +683,7 @@ def print_clone_commands(git_path, projects, port): if clone_path == ".": clone_path = "erplibre" lines.append( - f" git clone {base_url}/{repo_name}" - f" {clone_path}" + f" git clone {base_url}/{repo_name}" f" {clone_path}" ) lines.sort() for line in lines: diff --git a/script/git/github_api.py b/script/git/github_api.py index 2431c76..9ebbadf 100644 --- a/script/git/github_api.py +++ b/script/git/github_api.py @@ -26,17 +26,10 @@ def get_pull_request_repo( parsed_url = parse(upstream_url) status, user = gh.user.get() - user_name = ( - user["login"] if not organization_name else organization_name - ) - status, lst_pull = ( - gh.repos[user_name][parsed_url.repo].pulls.get() - ) + user_name = user["login"] if not organization_name else organization_name + status, lst_pull = gh.repos[user_name][parsed_url.repo].pulls.get() if type(lst_pull) is dict: - print( - f"For url {upstream_url}," - f" got {lst_pull.get('message')}" - ) + print(f"For url {upstream_url}," f" got {lst_pull.get('message')}") return False else: for pull in lst_pull: @@ -53,26 +46,21 @@ def fork_repo( parsed_url = parse(upstream_url) status, user = gh.user.get() - user_name = ( - user["login"] if not organization_name else organization_name - ) - status, forked_repo = ( - gh.repos[user_name][parsed_url.repo].get() - ) + user_name = user["login"] if not organization_name else organization_name + status, forked_repo = gh.repos[user_name][parsed_url.repo].get() if status == 404: - status, upstream_repo = ( - gh.repos[parsed_url.owner][parsed_url.repo].get() - ) + status, upstream_repo = gh.repos[parsed_url.owner][ + parsed_url.repo + ].get() if status == 404: print("Unable to find repo %s" % upstream_url) exit(1) args = {} if organization_name: args["organization"] = organization_name - status, forked_repo = ( - gh.repos[parsed_url.owner][parsed_url.repo] - .forks.post(**args) - ) + status, forked_repo = gh.repos[parsed_url.owner][ + parsed_url.repo + ].forks.post(**args) if status == 404: print( f"{Fore.RED}Error{Style.RESET_ALL} when forking" @@ -82,23 +70,16 @@ def fork_repo( else: try: print( - "Forked %s to %s" - % (upstream_url, forked_repo["html_url"]) + "Forked %s to %s" % (upstream_url, forked_repo["html_url"]) ) except Exception as e: print(e) print(forked_repo) print(upstream_url) elif status == 202: - print( - "Forked repo %s already exists" - % forked_repo["full_name"] - ) + print("Forked repo %s already exists" % forked_repo["full_name"]) elif status != 200: - print( - "Status not supported: %s - %s" - % (status, forked_repo) - ) + print("Status not supported: %s - %s" % (status, forked_repo)) exit(1) @@ -110,9 +91,7 @@ def add_and_fetch_remote( """ try: working_repo = Repo(repo_info.relative_path) - if repo_info.organization in [ - a.name for a in working_repo.remotes - ]: + if repo_info.organization in [a.name for a in working_repo.remotes]: print( f'Remote "{repo_info.organization}" already exist' f" in {repo_info.relative_path}" @@ -122,8 +101,7 @@ def add_and_fetch_remote( print(f"New repo {repo_info.relative_path}") if not root_repo: print( - "Missing git repository to root for repo" - f" {repo_info.path}" + "Missing git repository to root for repo" f" {repo_info.path}" ) return if branch_name: @@ -149,16 +127,14 @@ def add_and_fetch_remote( # Add remote upstream_remote = retry( wait_exponential_multiplier=1000, stop_max_delay=15000 - )(working_repo.create_remote)( - repo_info.organization, repo_info.url_https - ) + )(working_repo.create_remote)(repo_info.organization, repo_info.url_https) print( 'Remote "%s" created for %s' % (repo_info.organization, repo_info.url_https) ) # Fetch the remote - retry( - wait_exponential_multiplier=1000, stop_max_delay=15000 - )(upstream_remote.fetch)() + retry(wait_exponential_multiplier=1000, stop_max_delay=15000)( + upstream_remote.fetch + )() print('Remote "%s" fetched' % repo_info.organization) diff --git a/script/ide/pycharm_configuration.py b/script/ide/pycharm_configuration.py index 45dda12..c582d04 100755 --- a/script/ide/pycharm_configuration.py +++ b/script/ide/pycharm_configuration.py @@ -26,7 +26,9 @@ _logger = logging.getLogger(__name__) PROJECT_NAME = os.path.basename(os.getcwd()) IDEA_PATH = "./.idea" -DEFAULT_ODOO_BIN = "./odoo/odoo-bin" # Need this to replace static configuration +DEFAULT_ODOO_BIN = ( + "./odoo/odoo-bin" # Need this to replace static configuration +) IDEA_MISC = os.path.join(IDEA_PATH, "misc.xml") IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml") VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml") diff --git a/script/odoo/util/show_installed_module.py b/script/odoo/util/show_installed_module.py index 8de0454..df41542 100644 --- a/script/odoo/util/show_installed_module.py +++ b/script/odoo/util/show_installed_module.py @@ -2,7 +2,9 @@ # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -installed_modules = env["ir.module.module"].search([('state', '=', 'installed')]) +installed_modules = env["ir.module.module"].search( + [("state", "=", "installed")] +) print("Installed modules:") diff --git a/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py b/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py index c137af0..ccd2a8a 100644 --- a/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py +++ b/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py @@ -10,18 +10,20 @@ print("Running a script in the Odoo shell!") # ERROR: insert or update on table "discuss_channel_member" violates foreign key constraint "discuss_channel_member_channel_id_fkey" # DÉTAIL : Key (channel_id)=(3) is not present in table "discuss_channel". -env.cr.execute(""" +env.cr.execute( + """ ALTER TABLE discuss_channel DROP CONSTRAINT discuss_channel_channel_type_not_null; -""") +""" +) env.cr.commit() -#env.cr.execute("SELECT to_regclass('public.discuss_channel');") -#print("table:", env.cr.fetchone()) +# env.cr.execute("SELECT to_regclass('public.discuss_channel');") +# print("table:", env.cr.fetchone()) -#print( +# print( # "model discuss.channel:", # env["ir.model"].search([("model", "=", "discuss.channel")]), -#) +# ) print("End fix migration with update mail postgresql 17 to postgresql 18.") diff --git a/script/todo/todo.py b/script/todo/todo.py index 5fdfedd..b099569 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -393,7 +393,9 @@ class TODO: odoo_version_input = "" while odoo_version_input not in install_commands: if odoo_version_input: - print(f"{t('Error, cannot understand value')} '{odoo_version_input}'") + print( + f"{t('Error, cannot understand value')} '{odoo_version_input}'" + ) str_input_dyn_odoo_version = ( f"💬 {t('Choose a version:')}\n\t" + "\n\t".join([a[1] for a in install_commands.values()]) @@ -417,7 +419,9 @@ class TODO: cmd_intern, shell=True, executable="/bin/bash", check=True ) except subprocess.CalledProcessError as e: - print(f"{t('The Bash script failed with return code')} {e.returncode}.") + print( + f"{t('The Bash script failed with return code')} {e.returncode}." + ) print("Wait after installation and open projects by terminal.") print("make open_terminal") self.restart_script(str(e)) @@ -464,9 +468,7 @@ class TODO: bash_command = instance.get("bash_command") if bash_command: print(f"{t('Will execute:')} {bash_command}") - self.execute.exec_command_live( - bash_command, source_erplibre=False - ) + self.execute.exec_command_live(bash_command, source_erplibre=False) command = instance.get("Command:") if command: @@ -525,7 +527,9 @@ class TODO: int_cmd = int(status) if 1 < int_cmd <= init_len: cmd_no_found = False - status = click.confirm(t("Do you want a new instance?")) + status = click.confirm( + t("Do you want a new instance?") + ) instance = choices[int_cmd - 1] self.execute_from_configuration( instance, @@ -612,15 +616,9 @@ class TODO: print(t("Command not found !")) def prompt_execute_deploy(self): - print( - f"🤖 {t('Deploy ERPLibre to a local directory!')}" - ) + print(f"🤖 {t('Deploy ERPLibre to a local directory!')}") choices = [ - { - "prompt_description": t( - "Clone ERPLibre locally (git clone)" - ) - }, + {"prompt_description": t("Clone ERPLibre locally (git clone)")}, ] help_info = self.fill_help_info(choices) @@ -637,16 +635,12 @@ class TODO: def _deploy_clone_erplibre(self): default_path = os.path.expanduser("~/erplibre") target_path = ( - input( - t("Target directory path (default: ~/erplibre): ") - ).strip() + input(t("Target directory path (default: ~/erplibre): ")).strip() or default_path ) target_path = os.path.expanduser(target_path) if os.path.exists(target_path): - print( - f"{t('Directory already exists: ')}{target_path}" - ) + print(f"{t('Directory already exists: ')}{target_path}") return print(t("Cloning ERPLibre...")) cmd = ( @@ -656,13 +650,8 @@ class TODO: ) print(f"{t('Will execute:')} {cmd}") try: - self.execute.exec_command_live( - cmd, source_erplibre=False - ) - print( - f"{t('ERPLibre cloned successfully to: ')}" - f"{target_path}" - ) + self.execute.exec_command_live(cmd, source_erplibre=False) + print(f"{t('ERPLibre cloned successfully to: ')}" f"{target_path}") except Exception as e: print(f"{t('Error cloning ERPLibre: ')}{e}") @@ -762,18 +751,19 @@ class TODO: def _git_add_remote(self): remote_name = ( - input(t("Remote name (default: localhost): ")).strip() or "localhost" + input(t("Remote name (default: localhost): ")).strip() + or "localhost" ) - remote_url = input(t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ")).strip() + remote_url = input( + t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ") + ).strip() if not remote_url: print(t("Repository address is required!")) return cmd = f"git remote add {remote_name} {remote_url}" print(f"{t('Will execute:')} {cmd}") try: - self.execute.exec_command_live( - cmd, source_erplibre=False - ) + self.execute.exec_command_live(cmd, source_erplibre=False) print(t("Remote added successfully!")) except Exception as e: print(f"{t('Error adding remote: ')}{e}") @@ -781,8 +771,16 @@ class TODO: def prompt_execute_git_local_server(self): print(f"🤖 {t('Manage local git repository server!')}") choices = [ - {"prompt_description": t("Deploy a local git server (~/.git-server)")}, - {"prompt_description": t("Deploy a production git server (/srv/git, root required)")}, + { + "prompt_description": t( + "Deploy a local git server (~/.git-server)" + ) + }, + { + "prompt_description": t( + "Deploy a production git server (/srv/git, root required)" + ) + }, ] help_info = self.fill_help_info(choices) @@ -806,7 +804,11 @@ class TODO: ) print(f"🤖 {mode}") choices = [ - {"prompt_description": t("Run all (init + remote + push + serve)")}, + { + "prompt_description": t( + "Run all (init + remote + push + serve)" + ) + }, {"prompt_description": t("Init - Create bare repos")}, {"prompt_description": t("Remote - Add local remotes")}, {"prompt_description": t("Push - Push to local server")}, @@ -863,8 +865,16 @@ class TODO: print(f"🤖 {t('AI assistant tools for development!')}") choices = [ {"prompt_description": t("Configure Claude Code configurations")}, - {"prompt_description": t("Add an automation with Claude in todo.py")}, - {"prompt_description": t("RTK - CLI proxy to reduce LLM token consumption")}, + { + "prompt_description": t( + "Add an automation with Claude in todo.py" + ) + }, + { + "prompt_description": t( + "RTK - CLI proxy to reduce LLM token consumption" + ) + }, ] help_info = self.fill_help_info(choices) @@ -891,11 +901,7 @@ class TODO: "Todo Add Command - Add a command to todo.py menu" ) }, - { - "prompt_description": t( - "Show installed custom commands" - ) - }, + {"prompt_description": t("Show installed custom commands")}, ] help_info = self.fill_help_info(choices) @@ -926,9 +932,7 @@ class TODO: print(t("No custom commands found in ~/.claude/commands/")) return files = sorted( - f - for f in os.listdir(commands_dir) - if f.endswith(".md") + f for f in os.listdir(commands_dir) if f.endswith(".md") ) if not files: print(t("No custom commands found in ~/.claude/commands/")) @@ -944,10 +948,7 @@ class TODO: name = f[:-3] # remove .md print(f" /{name:<30} {date_str}") print("-" * 50) - print( - f"{t('Total:')}" - f" {len(files)}" - ) + print(f"{t('Total:')}" f" {len(files)}") def _setup_claude_command( self, command_name, template_filename, personalize=False @@ -996,14 +997,10 @@ class TODO: print(f"{t('Error creating file: ')}{e}") def _claude_add_automation(self): - description = input( - t("Description of the command to add: ") - ).strip() + description = input(t("Description of the command to add: ")).strip() if not description: return - command = input( - t("Bash command to execute: ") - ).strip() + command = input(t("Bash command to execute: ")).strip() if not command: return section = ( @@ -1013,9 +1010,7 @@ class TODO: or "git" ) section_key = f"{section}_from_makefile" - config_path = os.path.join( - os.path.dirname(__file__), "todo.json" - ) + config_path = os.path.join(os.path.dirname(__file__), "todo.json") try: with open(config_path) as f: config = json.load(f) @@ -1086,7 +1081,11 @@ class TODO: def prompt_execute_database(self): print(f"🤖 {t('Make changes to databases!')}") choices = [ - {"prompt_description": t("Download database to create backup (.zip)")}, + { + "prompt_description": t( + "Download database to create backup (.zip)" + ) + }, {"prompt_description": t("Restore from backup (.zip)")}, {"prompt_description": t("Create backup (.zip)")}, ] @@ -1134,7 +1133,9 @@ class TODO: print(t("Git daemon process killed.")) def prompt_execute_rtk(self): - print(f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}") + print( + f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}" + ) choices = [ {"prompt_description": t("Install RTK")}, {"prompt_description": t("Check RTK version")}, @@ -1170,7 +1171,11 @@ class TODO: choices = [ {"prompt_description": t("curl - Automatic install script")}, {"prompt_description": t("brew - Homebrew (macOS/Linux)")}, - {"prompt_description": t("cargo - Build from source (Rust required)")}, + { + "prompt_description": t( + "cargo - Build from source (Rust required)" + ) + }, ] help_info = self.fill_help_info(choices) status = click.prompt(help_info) @@ -1323,7 +1328,11 @@ class TODO: def prompt_execute_security(self): print(f"🤖 {t('Dependency security audit!')}") choices = [ - {"prompt_description": t("pip-audit - Check vulnerabilities on Python environments")}, + { + "prompt_description": t( + "pip-audit - Check vulnerabilities on Python environments" + ) + }, ] help_info = self.fill_help_info(choices) @@ -1368,12 +1377,16 @@ class TODO: return # Database name - db_name = input(t("Temporary database name (default: test_todo_tmp): ")).strip() + db_name = input( + t("Temporary database name (default: test_todo_tmp): ") + ).strip() if not db_name: db_name = "test_todo_tmp" # Extra modules - extra_modules = input(t("Extra modules to install (comma-separated, empty for none): ")).strip() + extra_modules = input( + t("Extra modules to install (comma-separated, empty for none): ") + ).strip() # Log level log_level = input(t("Log level (default: test): ")).strip() @@ -1395,9 +1408,7 @@ class TODO: ) # Step 2: Install modules - print( - f"\n--- {t('Installing modules')}: {modules_to_install} ---" - ) + print(f"\n--- {t('Installing modules')}: {modules_to_install} ---") cmd_install = ( f"./script/addons/install_addons.sh" f" {db_name} {modules_to_install}" @@ -1433,12 +1444,16 @@ class TODO: # Step 4: Cleanup lang = get_lang() - keep_input = input(t("Keep the temporary database? (y/N): ")).strip().lower() + keep_input = ( + input(t("Keep the temporary database? (y/N): ")).strip().lower() + ) keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes")) if keep: print(f"{t('Database kept')}: {db_name}") else: - print(f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---") + print( + f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---" + ) cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}" self.execute.exec_command_live( cmd_drop, @@ -1460,7 +1475,9 @@ class TODO: if status_code == 0: print(f"\n✅ {t('All unit tests passed')}") else: - print(f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}") + print( + f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}" + ) def execute_pip_audit(self): versions, installed_versions, odoo_installed_version = ( @@ -1495,7 +1512,11 @@ class TODO: } if not environments: - print(t("No installed environment found. Install an Odoo version first.")) + print( + t( + "No installed environment found. Install an Odoo version first." + ) + ) return # Show selection menu @@ -1508,7 +1529,9 @@ class TODO: env_input = "" while env_input not in environments and env_input != "0": if env_input: - print(f"{t('Error, cannot understand value')}" f" '{env_input}'") + print( + f"{t('Error, cannot understand value')}" f" '{env_input}'" + ) env_input = input(str_input).strip() if env_input == "0":