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"(?