From 6847891c1b9df3f4ef73fae26c00b890bb32b6fc Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 2 Sep 2026 06:35:05 -0400 Subject: [PATCH] =?UTF-8?q?[ADD]=20script=20todo=20:=20g=C3=A9rer=20les=20?= =?UTF-8?q?plugins=20Claude=20Code=20et=20leur=20liste=20ERPLibre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du menu. L'installation passe par « -y » : la sortie de TODO est un tuyau, pas un terminal, et la CLI refuse sans lui toute installation qui exécute une commande déclarée par un marketplace. La liste préférée s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses quatre plugins travaillent sur le poste, sans service tiers ni compte. La recherche lit les manifestes sur le disque, donc hors ligne. Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms dont l'un contient l'autre — une recherche naïve en rate trois. --- EN --- Nothing installed a plugin from TODO; the CLI alone did, outside the menu. Installing goes through « -y »: TODO's output is a pipe, not a terminal, and without it the CLI refuses any install that runs a command declared by a marketplace. The preferred list is therefore shown BEFORE the confirmation, the only chance to read it; its four plugins run on the workstation, with no third party and no account. Search reads the manifests from disk, hence offline. Checked: 8 new tests, among them the word boundary separating two names where one contains the other — a naive search misses three of them. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 232 +++++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 117 ++++++++++++++++++++ test/test_todo.py | 128 +++++++++++++++++++++ 3 files changed, 477 insertions(+) diff --git a/script/todo/todo.py b/script/todo/todo.py index a924289..5454edf 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -3027,6 +3027,11 @@ class TODO( ) }, {"prompt_description": t("Show the context given to Claude")}, + { + "prompt_description": t( + "Claude Code plugins - marketplaces and ERPLibre list" + ) + }, ] help_info = self.fill_help_info(choices) @@ -3043,6 +3048,8 @@ class TODO( self.prompt_execute_rtk() elif status == "4": self._show_claude_context() + elif status == "5": + self.prompt_execute_claude_plugins() else: print(t("Command not found !")) @@ -3334,6 +3341,231 @@ class TODO( except Exception as e: print(f"{t('Error adding automation: ')}{e}") + # Les plugins qu'ERPLibre pose par défaut, chacun avec la clé qui dit à + # quoi il sert. Tous viennent du marketplace officiel et travaillent sur + # le poste : aucun n'appelle un service tiers ni ne réclame de compte. + _CLAUDE_PREFERRED_PLUGINS = ( + ("superpowers", "brainstorming, subagent-driven development, TDD"), + ("pyright-lsp", "Python type checking and code intelligence"), + ("claude-security", "vulnerability scan run entirely in session"), + ( + "skill-creator", + "write, improve and evaluate the repository skills", + ), + ) + _CLAUDE_MARKETPLACES_DIR = "~/.claude/plugins/marketplaces" + + def prompt_execute_claude_plugins(self): + print(f"🤖 {t('Manage Claude Code plugins and marketplaces!')}") + choices = [ + {"section": t("Inventory")}, + {"prompt_description": t("List installed plugins")}, + {"prompt_description": t("List configured marketplaces")}, + {"prompt_description": t("Search a plugin in the marketplaces")}, + { + "prompt_description": t( + "Show a plugin detail and its token cost" + ) + }, + {"section": t("Install")}, + {"prompt_description": t("Install the ERPLibre preferred list")}, + {"prompt_description": t("Install a plugin by name")}, + {"prompt_description": t("Add a marketplace")}, + {"section": t("Maintenance")}, + { + "prompt_description": t( + "Update the marketplaces and the plugins" + ) + }, + {"prompt_description": t("Uninstall a plugin")}, + ] + help_info = self.fill_help_info(choices) + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + elif status == "1": + self._claude_plugin_exec("list") + elif status == "2": + self._claude_plugin_exec("marketplace list") + elif status == "3": + self._claude_plugin_search() + elif status == "4": + self._claude_plugin_details() + elif status == "5": + self._claude_install_preferred_plugins() + elif status == "6": + self._claude_plugin_install_by_name() + elif status == "7": + self._claude_marketplace_add() + elif status == "8": + self._claude_plugin_update() + elif status == "9": + self._claude_plugin_uninstall() + else: + print(t("Command not found !")) + + def _claude_plugin_exec(self, args, quiet=False, capture=False): + """Lance « claude plugin », ou signale que claude est absent. + + Rend le code de sortie, ou le couple (code, lignes) quand capture est + vrai. Le code 1 sans sortie signale l'absence de l'exécutable : rien + n'a tourné, et l'appelant ne doit pas conclure à un échec de la + commande elle-même. + """ + claude = shutil.which("claude") + if claude is None: + print(t("The claude command is not in the PATH.")) + return (1, []) if capture else 1 + return self.execute.exec_command_live( + f"{shlex.quote(claude)} plugin {args}", + source_erplibre=False, + quiet=quiet, + return_status_and_output=capture, + ) + + def _claude_plugin_is_installed(self, name): + """Le plugin est-il déjà posé ? + + La liste est lue telle que la CLI l'écrit, et le nom y est cherché + comme un mot entier : « code-review » ne doit pas se reconnaître dans + « pr-review-toolkit ». Un doute rend faux, et l'installation qui suit + est de toute façon idempotente. + """ + result = self._claude_plugin_exec("list", quiet=True, capture=True) + if not isinstance(result, tuple) or result[0] != 0: + return False + motif = re.compile(rf"(?