From 91c835d57e95e82d328f1f9fe8f8b6b479f7f2c9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 12 Mar 2026 04:37:19 -0400 Subject: [PATCH] [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.",