diff --git a/amelioration.md b/amelioration.md new file mode 100644 index 0000000..dfc3e09 --- /dev/null +++ b/amelioration.md @@ -0,0 +1,750 @@ +# Proposition d'amélioration de l'installeur de skills + +## Résumé + +Le problème observé avec `ha-log-investigator` ne vient probablement pas d'une désynchronisation du dépôt, mais d'un écart entre : + +1. le **format réellement standard de Claude Code**, fondé sur un dossier de skill contenant un fichier d'entrée `SKILL.md` et éventuellement des fichiers de support ; +2. le **format interne historique choisi par ce dépôt**, fondé sur un fichier par agent (`claude-code.md`, `codex.md`, etc.) que l'installeur copie ensuite vers `SKILL.md`. + +`ha-log-investigator` suit le premier modèle. `install.sh` ne sait actuellement traiter que le second. + +--- + +## 1. Structure actuelle du dépôt + +La documentation locale du projet définit aujourd'hui cette convention : + +```text +skills/// + claude-code.md + gemini-cli.md + codex.md + hermes.md +``` + +Cette convention apparaît notamment dans : + +- `README.md` +- `docs/structure_repo.md` +- `docs/structure_skill.md` +- `docs/structure_script_install.md` +- `docs/superpowers/specs/2026-05-16-mes-skills-design.md` + +Le rôle de l'installeur est alors de copier un fichier source propre à l'agent vers la destination finale sous le nom `SKILL.md` : + +```text +.md -> ~/./skills///SKILL.md +``` + +Cette architecture est cohérente en interne, mais ce n'est pas la structure native d'un skill Claude Code complet. + +--- + +## 2. Structure réellement attendue par Claude Code + +La documentation officielle Claude Code décrit un skill comme un **dossier** dont le point d'entrée obligatoire est `SKILL.md` : + +```text +my-skill/ + SKILL.md + template.md + examples/ + scripts/ +``` + +Les fichiers annexes sont optionnels mais font partie du modèle prévu : modèles, exemples, scripts, documentation de référence. Cette structure permet à un skill d'être plus qu'un simple fichier Markdown. + +Le dossier actuel `skills/infra/ha-log-investigator/` correspond précisément à ce modèle : + +```text +ha-log-investigator/ + SKILL.md + references/ + scripts/ + templates/ +``` + +Il est donc valide du point de vue du format Claude Code, et même plus représentatif d'un skill avancé que les anciens exemples mono-fichier. + +--- + +## 3. Pourquoi `ha-log-investigator` n'est pas détecté aujourd'hui + +Dans `install.sh`, la fonction `scan_skills()` parcourt tous les fichiers Markdown puis déduit l'agent à partir du **nom du fichier** : + +```bash +agent_file="${rel##*/}" +agent="${agent_file%.md}" +``` + +Cela fonctionne pour : + +```text +claude-code.md -> claude-code +codex.md -> codex +``` + +Mais pour : + +```text +SKILL.md -> SKILL +``` + +l'installeur croit que l'agent s'appelle `SKILL`, puis le filtre car aucun agent détecté ne porte ce nom. + +Il existe donc deux problèmes distincts : + +1. **détection impossible** des skills au format standard `SKILL.md` ; +2. **installation incomplète** des skills groupés, car le script copie actuellement un seul fichier et ne sait pas recopier les ressources associées (`scripts/`, `templates/`, `references/`). + +Le second point est important : même si l'on rendait simplement `SKILL.md` visible dans le menu, une copie limitée au fichier principal casserait une partie de la valeur de `ha-log-investigator`. + +--- + +## 4. Diagnostic d'architecture + +Le projet mélange aujourd'hui deux niveaux qui devraient être distingués : + +### A. Le format de stockage dans le dépôt + +Actuellement : + +```text +un fichier par agent +``` + +Avantage : simple à filtrer et à copier. + +Limite : ne représente pas correctement les skills modernes composés de plusieurs fichiers. + +### B. Le format final attendu par les agents + +Pour Claude Code, le format final naturel est déjà : + +```text +un dossier de skill avec SKILL.md et ressources facultatives +``` + +L'installeur convertit donc artificiellement un modèle interne simplifié vers le modèle final. Cette conversion fonctionne pour les skills simples, mais devient fragile dès qu'un skill devient réellement structuré. + +--- + +## 5. Proposition principale : passer à un modèle "bundle-first" + +### Principe + +Faire du dossier de skill le format canonique du dépôt : + +```text +skills/// + SKILL.md + scripts/ + templates/ + references/ +``` + +Le fichier `SKILL.md` devient la source principale. Le champ `agents:` du frontmatter indique les agents compatibles : + +```yaml +agents: [claude-code, codex] +``` + +L'installeur ne devrait plus déduire l'agent seulement depuis le nom d'un fichier, mais lire explicitement cette information dans le frontmatter. + +### Pourquoi ce modèle est meilleur + +- Il suit le standard Claude Code au lieu de le reconstruire indirectement. +- Il permet les vrais skills avancés avec scripts, références et templates. +- Il évite de dupliquer le même contenu entre plusieurs fichiers quand le skill est réellement commun à plusieurs agents. +- Il rend le dépôt plus lisible : un skill = un dossier complet. +- Il aligne mieux le stockage source et le format installé. + +--- + +## 6. Compatibilité recommandée pendant la transition + +Il ne faut pas forcément casser l'existant d'un coup. Une migration douce serait préférable. + +### Phase 1 — compatibilité double format + +L'installeur accepte les deux formes : + +```text +Format historique : +skills/dev/debugging/claude-code.md + +Format standard : +skills/infra/ha-log-investigator/SKILL.md +``` + +Règles proposées : + +1. si un dossier contient `SKILL.md`, il est traité comme un **bundle standard** ; +2. l'installeur lit `agents:` dans le frontmatter ; +3. il crée une entrée de menu par agent compatible détecté ; +4. lors de l'installation d'un bundle, il copie **tout le dossier utile**, pas seulement `SKILL.md` ; +5. les anciens fichiers `.md` restent supportés pour éviter une migration immédiate de tous les skills. + +### Phase 2 — migration progressive + +Au fil du temps : + +- convertir les skills simples vers `SKILL.md` quand c'est pertinent ; +- conserver des variantes par agent seulement lorsqu'un vrai écart de contenu est nécessaire ; +- mettre à jour les templates, les docs et les tests pour documenter le nouveau modèle comme format principal. + +--- + +## 7. Point à décider pour le multi-agent + +Le dépôt vise plusieurs agents. Il faut donc clarifier la politique suivante : + +### Option A — un `SKILL.md` commun multi-agent + +```text +ha-log-investigator/ + SKILL.md # agents: [claude-code, codex] +``` + +À privilégier quand le contenu est identique ou presque identique entre agents. + +### Option B — bundle commun + adaptations ciblées + +```text +ha-log-investigator/ + SKILL.md + variants/ + codex.md + hermes.md +``` + +À utiliser seulement si certains agents nécessitent réellement des différences de frontmatter ou de formulation. + +### Option C — rester entièrement par agent + +```text +ha-log-investigator/ + claude-code.md + codex.md +``` + +Simple mais moins adapté aux skills avec ressources partagées ; cette option devient moins convaincante pour les bundles riches. + +### Recommandation + +Adopter **A comme règle générale**, **B comme échappatoire**, et éviter **C** sauf pour les anciens skills en cours de migration. + +--- + +## 8. Améliorations concrètes à prévoir dans `install.sh` + +Sans détailler encore l'implémentation ligne par ligne, l'évolution devrait couvrir : + +1. **Découverte des skills** + - chercher les dossiers contenant `SKILL.md` ; + - continuer à reconnaître les anciens fichiers `.md`. + +2. **Lecture du frontmatter** + - parser `agents:` comme une liste réelle ; + - ne plus déduire systématiquement l'agent depuis le nom de fichier. + +3. **Installation** + - pour un bundle standard, recopier le dossier entier du skill ; + - préserver `references/`, `scripts/`, `templates/` et autres ressources ; + - éventuellement exclure les fichiers internes non destinés à l'installation si une convention est définie plus tard. + +4. **Affichage dans le menu** + - présenter un même skill pour chaque agent compatible détecté ; + - distinguer clairement les skills `bundle` des skills `legacy` si utile au diagnostic. + +5. **Tests** + - ajouter un test de détection d'un `SKILL.md` multi-agent ; + - ajouter un test d'installation avec ressources annexes ; + - ajouter un test de coexistence entre format historique et nouveau format. + +6. **Documentation** + - corriger `README.md`, `docs/structure_repo.md`, `docs/structure_skill.md` et `docs/structure_script_install.md` ; + - préciser que le format historique est encore accepté mais n'est plus le format recommandé. + +--- + +## 9. Cas particulier de `ha-log-investigator` + +`ha-log-investigator` est en réalité un bon révélateur d'un besoin déjà présent dans le projet : + +- il est plus complexe qu'un skill simple ; +- il contient des ressources annexes utiles ; +- il suit naturellement le standard moderne de Claude Code ; +- il met en évidence que l'architecture actuelle de l'installeur était suffisante pour des exemples simples, mais pas encore pour des skills complets. + +Il ne faudrait donc pas traiter ce dossier comme une anomalie à corriger, mais comme le premier cas concret justifiant l'évolution de l'installeur. + +--- + +## 10. Recommandation finale + +### Court terme + +Faire évoluer l'installeur pour supporter **sans casser l'existant** : + +- les anciens fichiers `.md` ; +- les bundles standards avec `SKILL.md` ; +- la copie récursive des ressources d'un bundle. + +### Moyen terme + +Faire du modèle suivant la convention officielle recommandée du dépôt : + +```text +skills///SKILL.md +``` + +avec `agents:` dans le frontmatter comme source de vérité. + +### Bénéfice attendu + +Le dépôt deviendrait : + +- plus fidèle aux standards réels des agents modernes ; +- plus robuste pour les skills évolués ; +- plus simple à maintenir à long terme ; +- moins dépendant d'une convention interne inventée uniquement pour faciliter le premier script. + +--- + +## 11. Sources consultées + +- Documentation officielle Claude Code, section consacrée aux skills : structure en dossier avec `SKILL.md` comme point d'entrée obligatoire et fichiers de support optionnels. +- Documentation interne du dépôt : `README.md`, `docs/structure_repo.md`, `docs/structure_skill.md`, `docs/structure_script_install.md`, `docs/superpowers/specs/2026-05-16-mes-skills-design.md`. + +--- + +# Plan technique proposé pour faire évoluer `install.sh` + +## Objectif du chantier + +Faire évoluer l'installeur pour qu'il sache gérer deux familles de skills : + +1. les **skills historiques mono-fichier**, encore présents dans le dépôt ; +2. les **skills bundle**, structurés selon le modèle `SKILL.md` + ressources associées. + +L'objectif n'est pas de réécrire tout le script, mais d'isoler proprement les changements nécessaires pour : + +- détecter `ha-log-investigator` ; +- l'installer correctement avec ses fichiers annexes ; +- préserver la compatibilité avec les skills existants ; +- préparer une migration progressive du dépôt vers le format moderne. + +--- + +## Étape 1 — Formaliser les deux formats supportés + +### 1.1 Format historique `legacy` + +```text +skills///.md +``` + +Exemples : + +```text +skills/dev/debugging/claude-code.md +skills/dev/debugging/codex.md +``` + +Caractéristiques : + +- un fichier = un agent ; +- l'agent est déduit du nom de fichier ; +- l'installation copie ce fichier vers `SKILL.md`. + +### 1.2 Format moderne `bundle` + +```text +skills///SKILL.md +skills///scripts/ +skills///templates/ +skills///references/ +``` + +Caractéristiques : + +- un dossier = un skill complet ; +- les agents compatibles sont lus dans `agents:` ; +- l'installation copie le dossier complet vers la destination. + +### Décision recommandée + +Documenter dès maintenant ces deux formats, mais annoncer clairement que : + +- `bundle` est le **format recommandé** pour les nouveaux skills ; +- `legacy` est un **format encore supporté pour compatibilité**. + +--- + +## Étape 2 — Faire évoluer le modèle interne utilisé par le script + +Aujourd'hui, `SKILLS_LIST` stocke des chaînes de ce type : + +```text +cat|skill|agent|etat|repo_version|local_version|desc|tags +``` + +Ce format ne dit pas : + +- d'où vient le skill ; +- s'il faut copier un fichier ou un dossier ; +- quel chemin source exact installer. + +### Nouveau format conseillé + +Étendre chaque entrée avec deux champs supplémentaires : + +```text +cat|skill|agent|etat|repo_version|local_version|desc|tags|kind|source_path +``` + +Avec : + +- `kind=legacy` pour les anciens fichiers `.md` ; +- `kind=bundle` pour les dossiers contenant `SKILL.md` ; +- `source_path` = chemin réel du fichier ou du dossier source. + +### Pourquoi cette étape est essentielle + +Elle évite d'éparpiller des suppositions partout dans le script. Une fois qu'une entrée sait si elle est `legacy` ou `bundle`, les étapes suivantes deviennent beaucoup plus simples : affichage, aperçu, installation, copie, tests. + +--- + +## Étape 3 — Séparer la découverte des skills en deux chemins explicites + +### 3.1 Ajouter une découverte des bundles + +Créer une logique dédiée qui parcourt : + +```bash +find "${REPO_DIR}/skills" -name "SKILL.md" +``` + +Pour chaque bundle : + +1. déterminer `cat` et `skill` depuis le chemin ; +2. lire `agents:` dans le frontmatter ; +3. créer une entrée par agent compatible détecté ; +4. stocker `kind=bundle` et `source_path=`. + +### 3.2 Conserver la découverte legacy + +Garder un second chemin pour les fichiers historiques : + +```bash +find "${REPO_DIR}/skills" -name "*.md" ! -name "SKILL.md" +``` + +Mais avec une règle importante : + +- ne considérer comme agent valide que les noms réellement connus (`claude-code`, `gemini-cli`, `codex`, `hermes`) ; +- ignorer les autres fichiers Markdown de support pour éviter les faux positifs. + +### 3.3 Éviter les doublons + +Si un même skill existe à la fois en `bundle` et en `legacy`, il faut décider une priorité. + +### Recommandation + +Priorité au format `bundle`. + +Exemple : si `skills/dev/foo/SKILL.md` existe déjà, les anciens fichiers `foo/claude-code.md` ne devraient plus créer de doublons pour le même agent, sauf si une convention de variante est introduite plus tard. + +--- + +## Étape 4 — Ajouter un vrai parseur minimal du champ `agents:` + +Aujourd'hui, l'installeur lit des champs simples comme `version:` ou `description:` avec `grep` et `awk`. + +Pour `agents: [claude-code, codex]`, il faut au minimum une fonction dédiée, par exemple conceptuellement : + +```text +get_frontmatter_agents(file) -> claude-code codex +``` + +### Ce que cette fonction doit savoir faire + +- lire une ligne du type `agents: [claude-code, codex]` ; +- retirer crochets et espaces ; +- retourner une liste exploitable ; +- ignorer proprement un fichier mal formé ; +- permettre au mode debug d'expliquer pourquoi un bundle est ignoré. + +### Recommandation + +Rester simple au début : supporter uniquement la forme compacte déjà utilisée dans ton dépôt. + +```yaml +agents: [claude-code, codex] +``` + +Pas besoin d'introduire tout de suite un vrai parseur YAML complet ; ce serait disproportionné pour ce script bash. + +--- + +## Étape 5 — Adapter la logique de version locale + +Aujourd'hui, `get_local_version()` sait lire : + +```text +/SKILL.md +``` + +Cette partie peut rester presque inchangée, parce qu'après installation les deux formats aboutissent à la même destination finale. + +### À vérifier néanmoins + +Pour un bundle, la version continue bien à être lue depuis : + +```text +/SKILL.md +``` + +Donc le modèle de comparaison de versions peut rester commun entre `legacy` et `bundle`. + +--- + +## Étape 6 — Faire évoluer l'installation elle-même + +C'est ici que la distinction `legacy` / `bundle` devient vraiment utile. + +### 6.1 Installation legacy + +Comportement actuel conservé : + +```text +copier .md vers /SKILL.md +``` + +### 6.2 Installation bundle + +Nouveau comportement : + +```text +copier le contenu du dossier source vers le dossier destination +``` + +Cela doit inclure : + +- `SKILL.md` +- `scripts/` +- `templates/` +- `references/` +- tout autre fichier de support réellement présent dans le bundle. + +### Point de vigilance + +Il faut décider si une mise à jour de bundle : + +- **écrase uniquement les fichiers présents** ; +- ou **resynchronise exactement** le dossier destination. + +### Recommandation + +Pour commencer, privilégier la solution la plus sûre : + +- créer le dossier si besoin ; +- recopier le contenu du bundle par-dessus ; +- ne pas supprimer automatiquement les fichiers supplémentaires présents côté destination. + +Cela évite de détruire des fichiers locaux non prévus. Une vraie option `--sync` ou `--clean` pourra venir plus tard si besoin. + +--- + +## Étape 7 — Adapter l'aperçu et l'affichage du menu + +### 7.1 Aperçu (`v`) + +Aujourd'hui, l'aperçu sait retrouver un fichier à afficher. Pour un bundle, il faut afficher : + +```text +/SKILL.md +``` + +et non le dossier lui-même. + +### 7.2 Affichage dans la liste + +Le menu peut rester presque identique. Il peut être utile d'ajouter, au moins en mode debug ou dans une future vue détaillée : + +- `legacy` +- `bundle` + +Mais je ne recommande pas d'ajouter trop de bruit dans l'interface principale dès la première étape. + +### 7.3 Description et tags + +Ils doivent toujours être lus depuis le fichier principal : + +- `.md` pour `legacy` ; +- `SKILL.md` pour `bundle`. + +--- + +## Étape 8 — Mettre à jour les tests avant la migration réelle + +Les tests actuels couvrent surtout : + +- les versions ; +- le parsing de champs simples ; +- les chemins ; +- l'état du menu. + +Ils ne couvrent pas encore le cœur du problème actuel. + +### Tests à ajouter + +1. **Détection d'un bundle simple** + - un dossier avec `SKILL.md` + - `agents: [claude-code]` + - attendu : une entrée détectée. + +2. **Détection d'un bundle multi-agent** + - `agents: [claude-code, codex]` + - attendu : deux entrées, une par agent. + +3. **Ignorer les fichiers de support Markdown** + - `references/foo.md` + - attendu : jamais interprété comme un skill installable. + +4. **Copie complète d'un bundle** + - présence de `SKILL.md`, `scripts/a.sh`, `templates/x.md` + - attendu : tous les fichiers sont présents après installation. + +5. **Compatibilité legacy conservée** + - un ancien `claude-code.md` + - attendu : toujours détecté et installable. + +6. **Priorité bundle sur legacy si les deux existent** + - même skill présent sous les deux formes + - attendu : pas de doublon incohérent. + +### Pourquoi avant la migration + +Parce qu'une fois ces tests en place, on peut faire évoluer `install.sh` avec beaucoup moins de risque de casser ce qui fonctionne déjà. + +--- + +## Étape 9 — Mettre à jour la documentation après validation du nouveau modèle + +Les fichiers à revoir sont clairement identifiés : + +- `README.md` +- `docs/structure_repo.md` +- `docs/structure_skill.md` +- `docs/structure_script_install.md` +- éventuellement `templates/` +- éventuellement `docs/superpowers/specs/...` si tu veux garder la spec historique comme document vivant plutôt qu'archive. + +### Message documentaire recommandé + +```text +Le format recommandé pour les nouveaux skills est désormais le bundle : +skills///SKILL.md + +Le format historique .md reste supporté pour compatibilité. +``` + +--- + +## Étape 10 — Migration des skills existants + +Je déconseille de migrer tous les skills dans le même commit que l'évolution de l'installeur. + +### Ordre conseillé + +1. faire accepter `bundle` + `legacy` par l'installeur ; +2. valider avec `ha-log-investigator` ; +3. migrer un skill simple, par exemple `docker-compose`, comme test réel ; +4. seulement ensuite décider si tu veux convertir tout le dépôt. + +### Pourquoi + +Cela te permet de vérifier le nouveau modèle avec : + +- un skill complexe (`ha-log-investigator`) ; +- un skill simple (`docker-compose`) ; +- et de voir si la duplication multi-agent reste réellement utile dans ta pratique. + +--- + +## Découpage recommandé en commits + +### Commit 1 — Support technique du double format + +- modèle interne enrichi ; +- scan `bundle` + `legacy` ; +- lecture de `agents:` ; +- tests de détection. + +### Commit 2 — Installation complète des bundles + +- copie récursive ; +- aperçu adapté ; +- tests d'installation bundle. + +### Commit 3 — Documentation + +- README ; +- docs de structure ; +- mention du format recommandé. + +### Commit 4 — Migration pilote + +- migration éventuelle d'un skill simple vers `SKILL.md` ; +- retour d'expérience avant généralisation. + +--- + +## Ce que je ne recommande pas pour l'instant + +### 1. Réécrire tout le script + +Le script actuel contient déjà beaucoup de logique utile : + +- détection des agents ; +- menu fzf ; +- gestion local/global ; +- comparaison de versions ; +- vue des skills installés. + +Le problème est ciblé ; une refonte complète serait plus risquée que nécessaire. + +### 2. Introduire un parseur YAML lourd + +Le besoin actuel est limité. Tant que le frontmatter reste sous contrôle dans ton propre dépôt, une fonction bash simple suffit. + +### 3. Migrer tous les anciens skills immédiatement + +Ce serait tentant, mais cela mélangerait : + +- changement d'architecture ; +- changement de données ; +- validation fonctionnelle. + +Mieux vaut d'abord stabiliser la mécanique. + +--- + +## Ordre de décision proposé + +Avant toute modification de code, il reste surtout trois décisions à valider : + +1. **Acceptes-tu que `bundle` devienne le format recommandé du dépôt ?** +2. **Veux-tu garder la compatibilité avec les anciens `.md` pendant une période de transition ?** +3. **Pour les bundles multi-agent, veux-tu partir sur un `SKILL.md` commun par défaut, avec variantes seulement si nécessaire ?** + +### Ma recommandation personnelle + +Oui aux trois : + +- `bundle` comme format recommandé ; +- compatibilité legacy conservée ; +- `SKILL.md` commun par défaut, variantes uniquement quand l'écart entre agents devient réel. diff --git a/install2.sh b/install2.sh new file mode 100755 index 0000000..e5cca99 --- /dev/null +++ b/install2.sh @@ -0,0 +1,914 @@ +#!/usr/bin/env bash +# install.sh — Installeur interactif de skills IA +# Dépôt : https://gitea.maison43.duckdns.org/gilles/mes_skills +set -euo pipefail + +# ── Couleurs Gruvbox Dark 256 ────────────────────────────────────── +# $'\033' = vrai octet ESC (0x1B) — fonctionne dans tableaux bash et printf sans echo -e +GRV_FG=$'\033[38;5;223m' +GRV_RED=$'\033[38;5;167m' +GRV_GREEN=$'\033[38;5;142m' +GRV_YELLOW=$'\033[38;5;214m' +GRV_BLUE=$'\033[38;5;109m' +GRV_PURPLE=$'\033[38;5;175m' +GRV_AQUA=$'\033[38;5;108m' +GRV_ORANGE=$'\033[38;5;208m' +GRV_GRAY=$'\033[38;5;245m' +RESET=$'\033[0m' + +# ── Thème fzf Gruvbox Dark ──────────────────────────────────────── +export FZF_DEFAULT_OPTS=" + --color=bg+:#3c3836,bg:#282828,spinner:#fb4934,hl:#928374 + --color=fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934 + --color=marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934 + --border=rounded --height=80% --layout=reverse +" + +# ── Icônes ──────────────────────────────────────────────────────── +ICO_OK="✓" +ICO_UPD="↑" +ICO_NEW="+" +ICO_NA="·" +ICO_LOCAL="●L" +ICO_GLOBAL="●G" +ICO_SKIP="○" + +# ── Configuration ───────────────────────────────────────────────── +REPO_URL="https://gitea.maison43.duckdns.org/gilles/mes_skills.git" +REPO_DIR="/tmp/mes_skills_$$" +_CLONED_REPO_DIR="" +STATE_FILE="/tmp/skills_state_$$" +COLLAPSED_FILE="/tmp/skills_collapsed_$$" + +SKILLS_DEBUG="${SKILLS_DEBUG:-0}" +SKILLS_DRY_RUN="${SKILLS_DRY_RUN:-0}" +SKILLS_REPO="${SKILLS_REPO:-}" +SKILLS_TAG="${SKILLS_TAG:-}" +SKILLS_AGENT="${SKILLS_AGENT:-}" + +# ── Helpers couleur ─────────────────────────────────────────────── +ok() { echo -e "${GRV_GREEN}${ICO_OK} $*${RESET}"; } +err() { echo -e "${GRV_RED}✗ $*${RESET}" >&2; } +info() { echo -e "${GRV_BLUE}→ $*${RESET}"; } +warn() { echo -e "${GRV_ORANGE}⚠ $*${RESET}"; } +debug() { [[ "$SKILLS_DEBUG" == "1" ]] && echo -e "${GRV_GRAY}[DBG] $*${RESET}" || true; } +header() { echo -e "\n${GRV_PURPLE}╔══ $* ══╗${RESET}\n"; } + +# ── Nettoyage automatique ───────────────────────────────────────── +cleanup() { + debug "Nettoyage $_CLONED_REPO_DIR et $STATE_FILE" + [[ -n "$_CLONED_REPO_DIR" && -d "$_CLONED_REPO_DIR" ]] && rm -rf "$_CLONED_REPO_DIR" + [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" + [[ -f "$COLLAPSED_FILE" ]] && rm -f "$COLLAPSED_FILE" +} +trap cleanup EXIT + +# ── Installation fzf ────────────────────────────────────────────── +_install_fzf_binary() { + local tmp_fzf="/tmp/fzf_$$.tar.gz" + info "Récupération de la version fzf depuis GitHub API..." + local fzf_ver + fzf_ver=$(curl -fsSL "https://api.github.com/repos/junegunn/fzf/releases/latest" \ + | grep '"tag_name"' | head -1 | awk -F'"' '{print $4}') + [[ -z "$fzf_ver" ]] && { err "Impossible de déterminer la version fzf."; exit 1; } + local fzf_ver_clean="${fzf_ver#v}" + local fzf_url="https://github.com/junegunn/fzf/releases/download/${fzf_ver}/fzf-${fzf_ver_clean}-linux_amd64.tar.gz" + info "Téléchargement fzf ${fzf_ver}..." + curl -fsSL "$fzf_url" -o "$tmp_fzf" + mkdir -p "$HOME/.local/bin" + tar -xzf "$tmp_fzf" -C "$HOME/.local/bin/" fzf + rm -f "$tmp_fzf" + export PATH="$HOME/.local/bin:$PATH" +} + +install_fzf() { + warn "fzf non trouvé." + echo -e " ${GRV_FG}Installer fzf ? [o/N]${RESET} \c" + read -r answer /dev/null; then + debug "Installation via apt" + apt-get install -y fzf 2>/dev/null || _install_fzf_binary + else + _install_fzf_binary + fi + command -v fzf &>/dev/null && ok "fzf installé." || { err "Impossible d'installer fzf."; exit 1; } +} + +# ── Vérification des dépendances ────────────────────────────────── +check_deps() { + header "Vérification des dépendances" + command -v git &>/dev/null || { err "git non trouvé. Installer git et relancer."; exit 1; } + ok "git $(git --version | awk '{print $3}')" + command -v fzf &>/dev/null || install_fzf + ok "fzf $(fzf --version | awk '{print $1}')" +} + +# ── Détection des agents IA ─────────────────────────────────────── +DETECTED_AGENTS=() + +detect_agents() { + header "Détection des agents IA" + + _add_agent() { + local name="$1" + if [[ -n "$SKILLS_AGENT" && "$SKILLS_AGENT" != "$name" ]]; then + debug "Agent $name ignoré (SKILLS_AGENT=$SKILLS_AGENT)" + return + fi + DETECTED_AGENTS+=("$name") + ok "Agent détecté : $name" + } + + _skip_agent() { + local name="$1" + if [[ -z "$SKILLS_AGENT" || "$SKILLS_AGENT" == "$name" ]]; then + echo -e "${GRV_GRAY}${ICO_NA} Agent absent : $name${RESET}" + fi + } + + _detect_gemini() { + command -v gemini &>/dev/null && return 0 + local prefix + prefix=$(npm config get prefix 2>/dev/null) || return 1 + [[ -n "$prefix" && -f "${prefix}/bin/gemini" ]] + } + + # claude-code + if [[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null; then + _add_agent "claude-code" + else + _skip_agent "claude-code" + fi + + # gemini-cli + if _detect_gemini; then + _add_agent "gemini-cli" + else + _skip_agent "gemini-cli" + fi + + # codex + if command -v codex &>/dev/null || [[ -f "$HOME/.npm-global/bin/codex" ]]; then + _add_agent "codex" + else + _skip_agent "codex" + fi + + # hermes + if command -v hermes &>/dev/null || [[ -f "$HOME/.local/bin/hermes" ]]; then + _add_agent "hermes" + else + _skip_agent "hermes" + fi + + if [[ ${#DETECTED_AGENTS[@]} -eq 0 ]]; then + warn "Aucun agent IA détecté. L'installation continuera mais aucun skill ne sera filtré." + fi +} + +# ── Sélection des agents (confirmation après auto-détection) ────── +select_agents() { + header "Pour quel(s) agent(s) installer ?" + + local -a agent_lines=() + for agent in "claude-code" "gemini-cli" "codex" "hermes"; do + local detected=0 + for a in "${DETECTED_AGENTS[@]}"; do [[ "$a" == "$agent" ]] && detected=1; done + if [[ $detected -eq 1 ]]; then + agent_lines+=("${agent}"$'\t'"${GRV_GREEN}✓ détecté ${RESET}${GRV_FG}${agent}${RESET}") + else + agent_lines+=("${agent}"$'\t'"${GRV_GRAY}○ non installé ${agent}${RESET}") + fi + done + + local raw_selected + raw_selected=$(printf '%s\n' "${agent_lines[@]}" | fzf \ + --multi \ + --ansi \ + --delimiter='\t' \ + --with-nth=2.. \ + --prompt="Agents > " \ + --bind="esc:abort" \ + --header="$(echo -e "${GRV_GRAY}TAB=sélectionner/désélectionner ENTER=valider ESC=quitter${RESET}")") || { echo; err "Annulé."; exit 0; } + + DETECTED_AGENTS=() + while IFS=$'\t' read -r agent_name _; do + [[ -n "$agent_name" ]] && DETECTED_AGENTS+=("$agent_name") + done <<< "$raw_selected" + + if [[ ${#DETECTED_AGENTS[@]} -eq 0 ]]; then + warn "Aucun agent sélectionné — tous les skills seront affichés." + else + ok "Agents sélectionnés : ${DETECTED_AGENTS[*]}" + fi +} + +# ── Clone du dépôt ──────────────────────────────────────────────── +clone_repo() { + header "Récupération du dépôt" + if [[ -n "$SKILLS_REPO" ]]; then + REPO_DIR="$SKILLS_REPO" + info "Utilisation du dépôt local : $REPO_DIR" + return + fi + info "Clonage depuis $REPO_URL..." + git clone --depth=1 "$REPO_URL" "$REPO_DIR" &>/dev/null + _CLONED_REPO_DIR="$REPO_DIR" + ok "Dépôt cloné dans $REPO_DIR" +} + +# ── Gestion des versions ────────────────────────────────────────── +get_frontmatter_field() { + local file="$1" field="$2" + grep "^${field}:" "$file" 2>/dev/null | head -1 | awk '{print $2}' | tr -d "\"'" +} + +get_frontmatter_desc() { + awk ' + /^description:[[:space:]]*[>|][[:space:]]*$/ { + in_desc=1 + next + } + /^description:/ { + sub(/^description:[[:space:]]*/, "") + print + exit + } + in_desc && /^[[:space:]]+/ { + sub(/^[[:space:]]+/, "") + printf "%s%s", sep, $0 + sep=" " + next + } + in_desc { + exit + } + ' "$1" 2>/dev/null | tr -d "\"'" | tr '|' ',' | cut -c1-55 +} + +get_frontmatter_tags() { + grep "^tags:" "$1" 2>/dev/null | head -1 \ + | sed 's/^tags:[[:space:]]*//' | tr -d '[] ' | tr ',' '#' | sed 's/^/#/' +} + +# Retourne les agents déclarés au format compact : agents: [claude-code, codex] +get_frontmatter_agents() { + grep "^agents:" "$1" 2>/dev/null | head -1 \ + | sed 's/^agents:[[:space:]]*//' | tr -d '[]' | tr ',' ' ' | xargs +} + +# Retourne 0 (succès) si ver2 est plus récente que ver1 +version_is_newer() { + local ver1="$1" ver2="$2" + [[ "$ver1" == "$ver2" ]] && return 1 + local newest + newest=$(printf '%s\n%s' "$ver1" "$ver2" | sort -V | tail -1) + [[ "$newest" == "$ver2" ]] +} + +# ── Chemins de destination ──────────────────────────────────────── +get_dest_path() { + local cat="$1" skill="$2" agent="$3" scope="$4" + local base + case "$agent" in + claude-code) [[ "$scope" == "global" ]] && base="$HOME/.claude" || base=".claude" ;; + gemini-cli) [[ "$scope" == "global" ]] && base="$HOME/.gemini" || base=".gemini" ;; + codex) [[ "$scope" == "global" ]] && base="$HOME/.codex" || base=".codex" ;; + hermes) [[ "$scope" == "global" ]] && base="$HOME/.hermes" || base=".hermes" ;; + esac + echo "${base}/skills/${cat}/${skill}/SKILL.md" +} + +get_local_version() { + local cat="$1" skill="$2" agent="$3" + local dest + dest=$(get_dest_path "$cat" "$skill" "$agent" "local") + [[ -f "$dest" ]] && get_frontmatter_field "$dest" "version" || echo "" +} + +# ── Scan des skills disponibles ─────────────────────────────────── +# Format : +# "cat|skill|agent|etat|repo_version|local_version|desc|tags|kind|source_path" +# kind = legacy (fichier .md) ou bundle (dossier avec SKILL.md) +SKILLS_LIST=() + +scan_skills() { + header "Scan des skills disponibles" + SKILLS_LIST=() + local -A seen_skills=() + + _append_skill_entry() { + local cat="$1" skill="$2" agent="$3" skill_file="$4" kind="$5" source_path="$6" + + # Filtre agent + local agent_detected=0 + if [[ ${#DETECTED_AGENTS[@]} -gt 0 ]]; then + for a in "${DETECTED_AGENTS[@]}"; do + [[ "$a" == "$agent" ]] && agent_detected=1 + done + fi + [[ "$agent_detected" -eq 0 && ${#DETECTED_AGENTS[@]} -gt 0 ]] && return + + # Filtre tag + if [[ -n "$SKILLS_TAG" ]]; then + grep -q "$SKILLS_TAG" "$skill_file" || return + fi + + local dedupe_key="${cat}|${skill}|${agent}" + [[ -n "${seen_skills[$dedupe_key]:-}" ]] && return + + local repo_ver; repo_ver=$(get_frontmatter_field "$skill_file" "version") + local local_ver; local_ver=$(get_local_version "$cat" "$skill" "$agent") + local etat + + if [[ -z "$local_ver" ]]; then + etat="new" + elif version_is_newer "$local_ver" "$repo_ver"; then + etat="upd" + else + etat="ok" + fi + + local desc; desc=$(get_frontmatter_desc "$skill_file") + local tags; tags=$(get_frontmatter_tags "$skill_file") + SKILLS_LIST+=("${cat}|${skill}|${agent}|${etat}|${repo_ver}|${local_ver}|${desc}|${tags}|${kind}|${source_path}") + seen_skills[$dedupe_key]=1 + debug "Skill trouvé : $cat/$skill [$agent] type=$kind état=$etat" + } + + # 1) Bundles modernes : un dossier avec SKILL.md comme point d'entrée. + while IFS= read -r skill_file; do + local rel="${skill_file#${REPO_DIR}/skills/}" + local skill_path="${rel%/*}" + local cat="${skill_path%%/*}" + local skill="${skill_path#*/}" + local source_dir="${skill_file%/SKILL.md}" + local agents; agents=$(get_frontmatter_agents "$skill_file") + + if [[ -z "$agents" ]]; then + debug "Bundle ignoré sans agents déclarés : $skill_file" + continue + fi + + local agent + for agent in $agents; do + _append_skill_entry "$cat" "$skill" "$agent" "$skill_file" "bundle" "$source_dir" + done + done < <(find "${REPO_DIR}/skills" -mindepth 3 -maxdepth 3 -name "SKILL.md" | sort) + + # 2) Format historique : un fichier par agent à la racine du dossier skill. + # Les bundles ont priorité grâce à seen_skills. + while IFS= read -r skill_file; do + local rel="${skill_file#${REPO_DIR}/skills/}" + local agent_file="${rel##*/}" + local agent="${agent_file%.md}" + local skill_path="${rel%/*}" + local cat="${skill_path%%/*}" + local skill="${skill_path#*/}" + + case "$agent" in + claude-code|gemini-cli|codex|hermes) ;; + *) debug "Fichier legacy ignoré (agent inconnu) : $skill_file"; continue ;; + esac + + _append_skill_entry "$cat" "$skill" "$agent" "$skill_file" "legacy" "$skill_file" + done < <(find "${REPO_DIR}/skills" -mindepth 3 -maxdepth 3 -name "*.md" ! -name "SKILL.md" | sort) + + ok "${#SKILLS_LIST[@]} skill(s) trouvé(s)" +} + +# ── Clé d'état normalisée ──────────────────────────────────────── +make_key() { + # Entrée : "cat|skill|agent|..." — Sortie : clé normalisée pour STATE_FILE + local entry="$1" + local cat skill agent + IFS='|' read -r cat skill agent _ <<< "$entry" + # Normalise en remplaçant - et / par _ pour éviter les collisions + printf '%s_%s_%s' "${cat//-/_}" "${skill//-/_}" "${agent//-/_}" +} + +# ── État du menu ────────────────────────────────────────────────── +state_init() { + : > "$STATE_FILE" + for entry in "${SKILLS_LIST[@]}"; do + local key; key=$(make_key "$entry") + echo "${key}=local" >> "$STATE_FILE" + done +} + +state_get() { + grep "^${1}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2 +} + +state_cycle() { + local key="$1" etat="$2" + local current; current=$(state_get "$key") + local next + case "$current" in + local) next="global" ;; + global) next="skip" ;; + skip) [[ "$etat" == "upd" ]] && next="update" || next="local" ;; + update) next="local" ;; + *) next="local" ;; + esac + sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE" +} + +# ── Formatage ligne skill (préfixe caché s:IDX\t pour fzf) ─────── +format_skill_line() { + local entry="$1" idx="$2" + local cat skill agent etat repo_ver local_ver desc tags + IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags <<< "$entry" + local key; key=$(make_key "$entry") + local action; action=$(state_get "$key") + + local ico_etat color_etat + case "$etat" in + ok) ico_etat="$ICO_OK"; color_etat="$GRV_GREEN" ;; + upd) ico_etat="$ICO_UPD"; color_etat="$GRV_YELLOW" ;; + new) ico_etat="$ICO_NEW"; color_etat="$GRV_AQUA" ;; + *) ico_etat="$ICO_NA"; color_etat="$GRV_GRAY" ;; + esac + + local ico_action color_action + case "$action" in + local) ico_action="$ICO_LOCAL"; color_action="$GRV_GREEN" ;; + global) ico_action="$ICO_GLOBAL"; color_action="$GRV_BLUE" ;; + skip) ico_action="$ICO_SKIP"; color_action="$GRV_GRAY" ;; + update) ico_action="$ICO_UPD"; color_action="$GRV_YELLOW" ;; + *) ico_action="$ICO_SKIP"; color_action="$GRV_GRAY" ;; + esac + + local ver_info="" + [[ "$etat" == "upd" ]] && ver_info=" (${local_ver}→${repo_ver})" + [[ "$etat" == "new" ]] && ver_info=" (v${repo_ver})" + + printf "s:%d\t ${color_etat}%s${RESET} %-24s ${GRV_GRAY}[%s]${RESET} ${color_action}%s${RESET}%s ${GRV_FG}%s${RESET} ${GRV_PURPLE}%s${RESET}\n" \ + "$idx" "$ico_etat" "${skill}" "$agent" "$ico_action" "$ver_info" "$desc" "$tags" +} + +# ── Formatage en-tête catégorie (préfixe caché h:CAT\t) ────────── +format_category_header() { + local cat="$1" count="$2" collapsed="$3" + local arrow color_arrow + if [[ "$collapsed" == "1" ]]; then + arrow="▶"; color_arrow="$GRV_YELLOW" + else + arrow="▼"; color_arrow="$GRV_BLUE" + fi + printf "h:%s\t${color_arrow}%s${RESET} ${GRV_BLUE}%s/${RESET} ${GRV_GRAY}(%d skill(s))${RESET}\n" \ + "$cat" "$arrow" "$cat" "$count" +} + +# ── Menu fzf principal ──────────────────────────────────────────── +run_menu() { + header "Sélection des skills" + echo -e " ${GRV_FG}Navigation :${RESET} ${GRV_YELLOW}↑↓${RESET} déplacer ${GRV_GREEN}SPACE${RESET} changer action ${GRV_GREEN}TAB${RESET} plier/déplier ${GRV_GREEN}v${RESET} voir skill ${GRV_GREEN}ENTER${RESET} confirmer ${GRV_RED}ESC${RESET} annuler" + echo -e " ${GRV_GRAY}Taper du texte filtre par nom ou description.${RESET}\n" + + state_init + + # Catégories avec >3 skills repliées par défaut + rm -f "$COLLAPSED_FILE" + declare -A _cat_count + for entry in "${SKILLS_LIST[@]}"; do + local _cat; _cat="${entry%%|*}" + _cat_count[$_cat]=$(( ${_cat_count[$_cat]:-0} + 1 )) + done + for _cat in "${!_cat_count[@]}"; do + [[ ${_cat_count[$_cat]} -gt 3 ]] && echo "$_cat" >> "$COLLAPSED_FILE" + done + + local space_script="/tmp/skills_space_$$.sh" + local fold_script="/tmp/skills_fold_$$.sh" + local tab_script="/tmp/skills_tab_$$.sh" + local list_script="/tmp/skills_list_$$.sh" + local fns_file="/tmp/skills_fns_$$.sh" + local preview_script="/tmp/skills_preview_$$.sh" + local copy_script="/tmp/skills_copy_$$.sh" + local mode_file="/tmp/skills_mode_$$.txt" + echo "repo" > "$mode_file" + + # Exporter fonctions et données dans un fichier source partagé + { + declare -f format_skill_line format_category_header state_get make_key + declare -p GRV_GREEN GRV_YELLOW GRV_AQUA GRV_GRAY GRV_BLUE GRV_FG GRV_PURPLE RESET \ + ICO_OK ICO_UPD ICO_NEW ICO_NA ICO_LOCAL ICO_GLOBAL ICO_SKIP + echo "STATE_FILE='$STATE_FILE'" + echo "REPO_DIR='$REPO_DIR'" + echo "COLLAPSED_FILE='$COLLAPSED_FILE'" + echo "MODE_FILE='$mode_file'" + echo "SKILLS_LIST=($(printf '"%s" ' "${SKILLS_LIST[@]}"))" + echo "DETECTED_AGENTS=($(printf '"%s" ' "${DETECTED_AGENTS[@]}"))" + } > "$fns_file" + + # Script SPACE : cycle action sur une ligne skill (s:IDX) + cat > "$space_script" << 'SPACE_EOF' +#!/usr/bin/env bash +source "FNSFILE" +td="$1"; type="${td%%:*}"; value="${td#*:}" +[[ "$type" != "s" ]] && exit 0 +entry="${SKILLS_LIST[$value]:-}" +[[ -z "$entry" ]] && exit 0 +key=$(make_key "$entry") +etat=$(echo "$entry" | cut -d'|' -f4) +current=$(grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2) +case "$current" in + local) next="global" ;; + global) next="skip" ;; + skip) [[ "$etat" == "upd" ]] && next="update" || next="local" ;; + update) next="local" ;; + *) next="local" ;; +esac +sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE" +SPACE_EOF + sed -i "s|FNSFILE|$fns_file|" "$space_script" + chmod +x "$space_script" + + # Script X : plier/déplier une catégorie (anciennement TAB) + cat > "$fold_script" << 'FOLD_EOF' +#!/usr/bin/env bash +source "FNSFILE" +td="$1"; type="${td%%:*}"; value="${td#*:}" +if [[ "$type" == "h" ]]; then + cat_name="$value" +elif [[ "$type" == "s" ]]; then + entry="${SKILLS_LIST[$value]:-}" + [[ -z "$entry" ]] && exit 0 + cat_name="${entry%%|*}" +else + exit 0 +fi +if grep -qx "$cat_name" "$COLLAPSED_FILE" 2>/dev/null; then + sed -i "/^${cat_name}$/d" "$COLLAPSED_FILE" +else + echo "$cat_name" >> "$COLLAPSED_FILE" +fi +FOLD_EOF + sed -i "s|FNSFILE|$fns_file|" "$fold_script" + chmod +x "$fold_script" + + # Script TAB : basculer entre section DÉPÔT et section GLOBAL + cat > "$tab_script" << 'TAB_EOF' +#!/usr/bin/env bash +source "FNSFILE" +mode=$(cat "$MODE_FILE" 2>/dev/null || echo "repo") +[[ "$mode" == "repo" ]] && echo "global" > "$MODE_FILE" || echo "repo" > "$MODE_FILE" +TAB_EOF + sed -i "s|FNSFILE|$fns_file|" "$tab_script" + chmod +x "$tab_script" + + # Script LIST : deux sections (DÉPÔT / GLOBAL) avec bascule via MODE_FILE + cat > "$list_script" << 'LIST_EOF' +#!/usr/bin/env bash +source "FNSFILE" + +mode=$(cat "$MODE_FILE" 2>/dev/null || echo "repo") + +declare -A agent_dir_map +agent_dir_map[claude-code]="$HOME/.claude" +agent_dir_map[gemini-cli]="$HOME/.gemini" +agent_dir_map[codex]="$HOME/.codex" +agent_dir_map[hermes]="$HOME/.hermes" + +gen_repo_section() { + local active="$1" + if [[ "$active" == "1" ]]; then + printf "d:section-repo\t${GRV_PURPLE}╔══ 📦 DÉPÔT — Skills disponibles ══╗${RESET}\n" + else + printf "d:section-repo\t${GRV_GRAY}── 📦 DÉPÔT — Skills disponibles ── (TAB pour basculer)${RESET}\n" + fi + declare -A cat_map + declare -a cat_order + for i in "${!SKILLS_LIST[@]}"; do + cat="${SKILLS_LIST[$i]%%|*}" + [[ -z "${cat_map[$cat]+x}" ]] && { cat_order+=("$cat"); cat_map[$cat]=""; } + cat_map[$cat]+=" $i" + done + for cat in "${cat_order[@]}"; do + indices=(${cat_map[$cat]}) + if [[ "$active" == "0" ]]; then + format_category_header "$cat" "${#indices[@]}" "1" + else + collapsed=0 + grep -qx "$cat" "$COLLAPSED_FILE" 2>/dev/null && collapsed=1 + format_category_header "$cat" "${#indices[@]}" "$collapsed" + [[ "$collapsed" == "1" ]] && continue + for idx in "${indices[@]}"; do + format_skill_line "${SKILLS_LIST[$idx]}" "$idx" + done + fi + done +} + +gen_global_section() { + local active="$1" + if [[ "$active" == "1" ]]; then + printf "d:section-global\t${GRV_PURPLE}╔══ 💾 GLOBAL — Skills installés ══╗${RESET}\n" + else + printf "d:section-global\t${GRV_GRAY}── 💾 GLOBAL — Skills installés ── (TAB pour basculer)${RESET}\n" + fi + local -a global_lines=() + for agent in "${DETECTED_AGENTS[@]}"; do + base="${agent_dir_map[$agent]:-}" + [[ -z "$base" ]] && continue + while IFS= read -r skill_md; do + rel="${skill_md#${base}/skills/}" + cat_name="${rel%%/*}"; rest="${rel#*/}"; skill_name="${rest%%/*}" + ver=$(grep "^version:" "$skill_md" 2>/dev/null | head -1 | awk '{print $2}') + global_lines+=("g:${agent}|${cat_name}|${skill_name}\t ${GRV_GRAY}${cat_name}/${skill_name}${RESET} ${GRV_GRAY}[${agent}]${RESET} ${GRV_GRAY}v${ver:-?}${RESET}") + done < <(find "${base}/skills" -name "SKILL.md" 2>/dev/null | sort) + done + if [[ ${#global_lines[@]} -eq 0 ]]; then + printf "d:-\t${GRV_GRAY} (aucun skill installé globalement)${RESET}\n" + else + for line in "${global_lines[@]}"; do + printf '%s\n' "$line" + done + fi +} + +sep() { printf "d:sep\t${GRV_GRAY}────────────────────────────────────────────────────────${RESET}\n"; } + +if [[ "$mode" == "repo" ]]; then + gen_repo_section 1 + sep + gen_global_section 0 +else + gen_global_section 1 + sep + gen_repo_section 0 +fi +LIST_EOF + sed -i "s|FNSFILE|$fns_file|" "$list_script" + chmod +x "$list_script" + + # Script PREVIEW : affiche le skill avec coloration (bat si dispo) + cat > "$preview_script" << 'PREVIEW_EOF' +#!/usr/bin/env bash +source "FNSFILE" +td="$1"; type="${td%%:*}"; value="${td#*:}" +if [[ "$type" == "s" ]]; then + entry="${SKILLS_LIST[$value]:-}" + [[ -z "$entry" ]] && exit 0 + IFS='|' read -r cat skill agent _ _ _ _ _ kind source_path <<< "$entry" + if [[ "$kind" == "bundle" ]]; then + skill_file="${source_path}/SKILL.md" + else + skill_file="$source_path" + fi + if [[ ! -f "$skill_file" ]]; then echo "Fichier introuvable : $skill_file"; exit 0; fi + if command -v bat &>/dev/null; then + bat --style=numbers,header --color=always --language=markdown "$skill_file" + elif command -v batcat &>/dev/null; then + batcat --style=numbers,header --color=always --language=markdown "$skill_file" + else + cat "$skill_file" + fi +elif [[ "$type" == "h" ]]; then + echo "=== Catégorie : ${value} ===" + for entry in "${SKILLS_LIST[@]}"; do + [[ "${entry%%|*}" == "$value" ]] || continue + IFS='|' read -r _ skill agent _ _ _ desc tags _ _ <<< "$entry" + echo " • ${skill} [${agent}] ${desc} ${tags}" + done +elif [[ "$type" == "g" || ( "$type" == "d" && "$value" != "-" && "$value" != "section-repo" && "$value" != "section-global" && "$value" != "sep" ) ]]; then + IFS='|' read -r agent cat_name skill_name <<< "$value" + case "$agent" in + claude-code) base="$HOME/.claude" ;; + gemini-cli) base="$HOME/.gemini" ;; + codex) base="$HOME/.codex" ;; + hermes) base="$HOME/.hermes" ;; + *) exit 0 ;; + esac + skill_file="${base}/skills/${cat_name}/${skill_name}/SKILL.md" + if [[ -f "$skill_file" ]]; then + if command -v bat &>/dev/null; then + bat --style=numbers,header --color=always --language=markdown "$skill_file" + elif command -v batcat &>/dev/null; then + batcat --style=numbers,header --color=always --language=markdown "$skill_file" + else + cat "$skill_file" + fi + else + echo "Fichier introuvable : $skill_file" + fi +fi +PREVIEW_EOF + sed -i "s|FNSFILE|$fns_file|" "$preview_script" + chmod +x "$preview_script" + + # Script de copie presse-papier (touche c) + cat > "$copy_script" << 'COPY_EOF' +#!/usr/bin/env bash +list_script="LISTFILE" +# Génère la liste, supprime le champ caché (avant le 1er tab) et les codes ANSI +content=$(bash "$list_script" | sed 's/^[^\t]*\t//' | sed 's/\x1b\[[0-9;]*[mGKHF]//g') +if command -v wl-copy &>/dev/null; then + printf '%s' "$content" | wl-copy +elif command -v xclip &>/dev/null; then + printf '%s' "$content" | xclip -selection clipboard +elif command -v xsel &>/dev/null; then + printf '%s' "$content" | xsel --clipboard --input +else + exit 1 +fi +COPY_EOF + sed -i "s|LISTFILE|$list_script|" "$copy_script" + chmod +x "$copy_script" + + # Fichier d'aide F1 + local help_file="/tmp/skills_help_$$.txt" + cat > "$help_file" << HELP_EOF +$(echo -e "${GRV_PURPLE}╔══════════════════════════════════════════════════════════╗ +║ AIDE — mes_skills installer (F1 pour fermer) ║ +╚══════════════════════════════════════════════════════════╝${RESET}") + +$(echo -e "${GRV_BLUE}NAVIGATION${RESET}") + $(echo -e "${GRV_YELLOW}↑ ↓${RESET}") Déplacer le curseur + $(echo -e "${GRV_YELLOW}Taper${RESET}") Filtrer par nom ou description + $(echo -e "${GRV_YELLOW}ENTER${RESET}") Confirmer les sélections et lancer l'installation + $(echo -e "${GRV_YELLOW}ESC${RESET}") Quitter sans installer + $(echo -e "${GRV_YELLOW}q${RESET}") Fermer cette aide + +$(echo -e "${GRV_BLUE}ÉTATS DES SKILLS${RESET}") + $(echo -e "${GRV_GREEN}✓${RESET}") Déjà installé (même version) + $(echo -e "${GRV_YELLOW}↑${RESET}") Mise à jour disponible (version dépôt > locale) + $(echo -e "${GRV_AQUA}+${RESET}") Nouveau skill (pas encore installé) + $(echo -e "${GRV_GRAY}·${RESET}") Non applicable (agent non sélectionné) + +$(echo -e "${GRV_BLUE}ACTIONS (SPACE pour cycler)${RESET}") + $(echo -e "${GRV_GREEN}●L${RESET}") Installer en LOCAL → .claude/skills/ (dossier courant) + $(echo -e "${GRV_BLUE}●G${RESET}") Installer en GLOBAL → ~/.claude/skills/ + $(echo -e "${GRV_GRAY}○${RESET}") Ignorer — ne pas installer ce skill + $(echo -e "${GRV_YELLOW}↑${RESET}") Mettre à jour (visible uniquement si MAJ disponible) + +$(echo -e "${GRV_BLUE}RACCOURCIS CLAVIER${RESET}") + $(echo -e "${GRV_GREEN}SPACE${RESET}") Changer l'action du skill sélectionné + $(echo -e "${GRV_GREEN}x${RESET}") Plier / déplier la catégorie + $(echo -e "${GRV_GREEN}TAB${RESET}") Basculer entre section DÉPÔT et section GLOBAL + $(echo -e "${GRV_GREEN}v${RESET}") Afficher / masquer le contenu du skill (preview) — ferme aussi cette aide + $(echo -e "${GRV_GREEN}c${RESET}") Copier la liste affichée dans le presse-papier + $(echo -e "${GRV_GREEN}F1${RESET}") Ouvrir cette aide dans le panneau preview (v pour revenir au skill) + +$(echo -e "${GRV_BLUE}DEUX SECTIONS${RESET}") + $(echo -e "${GRV_PURPLE}╔══ 📦 DÉPÔT ══╗${RESET}") Section active — skills du dépôt (installables) + $(echo -e "${GRV_GRAY}── 💾 GLOBAL ──${RESET}") Section inactive — skills déjà installés + +$(echo -e "${GRV_BLUE}ARBRE DES CATÉGORIES${RESET}") + $(echo -e "${GRV_BLUE}▼${RESET} dev/") Catégorie dépliée — x pour replier + $(echo -e "${GRV_YELLOW}▶${RESET} infra/") Catégorie repliée — x pour déplier + $(echo -e "${GRV_GRAY}Les catégories avec >3 skills sont repliées par défaut.${RESET}") + +$(echo -e "${GRV_BLUE}VARIABLES D'ENVIRONNEMENT${RESET}") + $(echo -e "${GRV_FG}SKILLS_AGENT=claude${RESET}") Forcer un seul agent + $(echo -e "${GRV_FG}SKILLS_TAG=bash${RESET}") Filtrer par tag + $(echo -e "${GRV_FG}SKILLS_DRY_RUN=1${RESET}") Simuler sans écrire + $(echo -e "${GRV_FG}SKILLS_DEBUG=1${RESET}") Affichage détaillé + $(echo -e "${GRV_FG}SKILLS_REPO=/chemin${RESET}") Utiliser un dépôt local + +$(echo -e "${GRV_GRAY}─────────────────────────────────────────────────────────${RESET}") +$(echo -e "${GRV_GRAY}Dépôt : https://gitea.maison43.duckdns.org/gilles/mes_skills${RESET}") +HELP_EOF + + local legend + legend=$(echo -e "${GRV_GRAY}État: ${GRV_GREEN}✓ ${GRV_YELLOW}↑ ${GRV_AQUA}+ Action: ${GRV_GREEN}●L ${GRV_BLUE}●G ${GRV_GRAY}○ SPACE=action x=plier TAB=sections v=voir c=copier F1=aide ENTER=ok ESC=quitter${RESET}") + + fzf \ + --ansi \ + --delimiter='\t' \ + --with-nth=2.. \ + --nth=2.. \ + --prompt="Skills > " \ + --header="$legend" \ + --preview="bash $preview_script {1}" \ + --preview-window="right:50%:wrap:hidden" \ + --bind="space:execute-silent(bash $space_script {1})+reload(bash $list_script)+pos({n})" \ + --bind="x:execute-silent(bash $fold_script {1})+reload(bash $list_script)+pos({n})" \ + --bind="tab:execute-silent(bash $tab_script)+reload(bash $list_script)+first" \ + --bind="v:change-preview(bash $preview_script {1})+toggle-preview" \ + --bind="f1:change-preview(cat $help_file)+show-preview" \ + --bind="c:execute-silent(bash $copy_script)" \ + < <(bash "$list_script") > /dev/null || true + + rm -f "$space_script" "$fold_script" "$tab_script" "$list_script" "$fns_file" "$preview_script" "$copy_script" "$help_file" "$mode_file" +} + +# ── Installation ────────────────────────────────────────────────── +install_selected() { + header "Installation" + local count_install=0 count_update=0 count_skip=0 + + for entry in "${SKILLS_LIST[@]}"; do + local cat skill agent etat repo_ver local_ver desc tags kind source_path + IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags kind source_path <<< "$entry" + local key; key=$(make_key "$entry") + local action; action=$(state_get "$key") + + if [[ "$action" == "skip" ]]; then + (( count_skip++ )) || true + continue + fi + + local scope="$action" + [[ "$action" == "update" ]] && scope="local" + + local dest; dest=$(get_dest_path "$cat" "$skill" "$agent" "$scope") + + if [[ "$kind" == "bundle" ]]; then + local dest_dir; dest_dir=$(dirname "$dest") + debug "Copie bundle $source_path → $dest_dir" + if [[ "$SKILLS_DRY_RUN" == "1" ]]; then + info "[DRY-RUN] cp -a $source_path/. → $dest_dir/" + else + mkdir -p "$dest_dir" + cp -a "$source_path/." "$dest_dir/" + fi + else + debug "Copie $source_path → $dest" + if [[ "$SKILLS_DRY_RUN" == "1" ]]; then + info "[DRY-RUN] cp $source_path → $dest" + else + mkdir -p "$(dirname "$dest")" + cp "$source_path" "$dest" + fi + fi + + if [[ "$action" == "update" ]]; then + ok "Mis à jour : ${cat}/${skill} [${agent}] ${local_ver}→${repo_ver}" + (( count_update++ )) || true + else + ok "Installé : ${cat}/${skill} [${agent}] → ${scope}" + (( count_install++ )) || true + fi + done + + echo -e "\n${GRV_PURPLE}╔══ Bilan ══╗${RESET}" + echo -e " ${GRV_GREEN}${ICO_OK} $count_install installé(s)${RESET}" + echo -e " ${GRV_YELLOW}${ICO_UPD} $count_update mis à jour${RESET}" + echo -e " ${GRV_GRAY}${ICO_SKIP} $count_skip ignoré(s)${RESET}" +} + +# ── Récapitulatif final ─────────────────────────────────────────── +print_summary() { + local shown=() + + echo -e "\n${GRV_PURPLE}╔══ Tester vos skills ══╗${RESET}\n" + for entry in "${SKILLS_LIST[@]}"; do + local cat skill agent etat repo_ver local_ver + IFS='|' read -r cat skill agent etat repo_ver local_ver <<< "$entry" + local key; key=$(make_key "$entry") + local action; action=$(state_get "$key") + [[ "$action" == "skip" ]] && continue + + local already=0 + if [[ ${#shown[@]} -gt 0 ]]; then + for s in "${shown[@]}"; do [[ "$s" == "${skill}|${agent}" ]] && already=1; done + fi + [[ "$already" -eq 1 ]] && continue + shown+=("${skill}|${agent}") + + case "$agent" in + claude-code) echo -e " ${GRV_AQUA}claude \"utilise le skill ${skill}\" --print${RESET}" ;; + gemini-cli) echo -e " ${GRV_AQUA}gemini -p \"utilise le skill ${skill}\"${RESET}" ;; + codex) echo -e " ${GRV_AQUA}codex \"\$${skill}\"${RESET}" ;; + hermes) echo -e " ${GRV_AQUA}hermes \"utilise le skill ${skill}\"${RESET}" ;; + esac + done + + echo -e "\n${GRV_PURPLE}╔══ Documentation agents ══╗${RESET}\n" + for agent in "${DETECTED_AGENTS[@]}"; do + case "$agent" in + claude-code) echo -e " ${GRV_BLUE}Claude Code${RESET} → https://code.claude.com/docs/en/skills" ;; + gemini-cli) echo -e " ${GRV_BLUE}Gemini CLI ${RESET} → https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/skills.md" ;; + codex) echo -e " ${GRV_BLUE}Codex ${RESET} → https://developers.openai.com/codex/skills" ;; + hermes) echo -e " ${GRV_BLUE}Hermes ${RESET} → https://hermes-agent.nousresearch.com/docs/user-guide/features/skills/" ;; + esac + done + echo "" +} + +# ── Point d'entrée ──────────────────────────────────────────────── +main() { + echo -e "\n${GRV_PURPLE}╔══════════════════════════════════════╗${RESET}" + echo -e "${GRV_PURPLE}║ mes_skills — Installeur de skills ║${RESET}" + echo -e "${GRV_PURPLE}╚══════════════════════════════════════╝${RESET}\n" + + check_deps + detect_agents + select_agents + clone_repo + scan_skills + + if [[ ${#SKILLS_LIST[@]} -eq 0 ]]; then + warn "Aucun skill compatible trouvé. Vérifier les agents détectés ou SKILLS_TAG." + exit 0 + fi + + run_menu + install_selected + print_summary +} + +main "$@" diff --git a/tests/test_install2.sh b/tests/test_install2.sh new file mode 100644 index 0000000..0c31407 --- /dev/null +++ b/tests/test_install2.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# Tests ciblés pour install2.sh +# Usage : cd tests && bash test_install2.sh +set -euo pipefail + +PASS=0 +FAIL=0 + +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo " ✓ $desc" + (( PASS++ )) || true + else + echo " ✗ $desc" + echo " attendu : '$expected'" + echo " obtenu : '$actual'" + (( FAIL++ )) || true + fi +} + +assert_true() { + local desc="$1" + shift + if eval "$@" 2>/dev/null; then + echo " ✓ $desc" + (( PASS++ )) || true + else + echo " ✗ $desc" + (( FAIL++ )) || true + fi +} + +load_install() { + local tmp; tmp=$(mktemp) + grep -v '^main "\$@"' ../install2.sh > "$tmp" + set +e + # shellcheck disable=SC1090 + source "$tmp" + set -e + rm -f "$tmp" +} + +echo "" +echo "══════════════════════════════════" +echo " Tests install2.sh" +echo "══════════════════════════════════" +echo "" + +load_install +trap - EXIT + +TMP_REPO=$(mktemp -d) +TMP_HOME=$(mktemp -d) +OLD_HOME="$HOME" +HOME="$TMP_HOME" +REPO_DIR="$TMP_REPO" +SKILLS_TAG="" +SKILLS_DEBUG=0 +DETECTED_AGENTS=(claude-code codex) + +mkdir -p \ + "$TMP_REPO/skills/infra/bundle-demo/scripts" \ + "$TMP_REPO/skills/infra/bundle-demo/templates" \ + "$TMP_REPO/skills/dev/legacy-demo" \ + "$TMP_REPO/skills/dev/mixed-demo" + +cat > "$TMP_REPO/skills/infra/bundle-demo/SKILL.md" <<'EOF' +--- +name: bundle-demo +version: 1.2.0 +description: > + Skill bundle de test + sur plusieurs lignes +agents: [claude-code, codex] +category: infra +tags: [bundle, test] +--- +# Bundle Demo +EOF +echo '#!/usr/bin/env bash' > "$TMP_REPO/skills/infra/bundle-demo/scripts/check.sh" +echo 'template' > "$TMP_REPO/skills/infra/bundle-demo/templates/report.md" + +cat > "$TMP_REPO/skills/dev/legacy-demo/claude-code.md" <<'EOF' +--- +name: legacy-demo +version: 1.0.0 +description: Skill legacy de test +agents: [claude-code] +category: dev +tags: [legacy] +--- +# Legacy Demo +EOF + +cat > "$TMP_REPO/skills/dev/mixed-demo/SKILL.md" <<'EOF' +--- +name: mixed-demo +version: 2.0.0 +description: Bundle prioritaire +agents: [claude-code] +category: dev +tags: [mixed] +--- +# Mixed Bundle +EOF + +cat > "$TMP_REPO/skills/dev/mixed-demo/claude-code.md" <<'EOF' +--- +name: mixed-demo +version: 1.0.0 +description: Legacy secondaire +agents: [claude-code] +category: dev +tags: [mixed] +--- +# Mixed Legacy +EOF + +echo "1. get_frontmatter_agents()" +assert_eq "agents multi-agent" \ + "claude-code codex" \ + "$(get_frontmatter_agents "$TMP_REPO/skills/infra/bundle-demo/SKILL.md")" +assert_eq "description YAML pliée" \ + "Skill bundle de test sur plusieurs lignes" \ + "$(get_frontmatter_desc "$TMP_REPO/skills/infra/bundle-demo/SKILL.md")" + +echo "" +echo "2. scan_skills()" +scan_skills >/dev/null +assert_eq "4 entrées détectées" "4" "${#SKILLS_LIST[@]}" +assert_true "bundle claude détecté" \ + "printf '%s\n' \"\${SKILLS_LIST[@]}\" | grep -q '^infra|bundle-demo|claude-code|.*|bundle|'" +assert_true "bundle codex détecté" \ + "printf '%s\n' \"\${SKILLS_LIST[@]}\" | grep -q '^infra|bundle-demo|codex|.*|bundle|'" +assert_true "legacy détecté" \ + "printf '%s\n' \"\${SKILLS_LIST[@]}\" | grep -q '^dev|legacy-demo|claude-code|.*|legacy|'" +assert_eq "bundle prioritaire sur legacy" \ + "1" \ + "$(printf '%s\n' "${SKILLS_LIST[@]}" | grep -c '^dev|mixed-demo|claude-code|')" + +echo "" +echo "3. install_selected()" +STATE_FILE=$(mktemp) +for entry in "${SKILLS_LIST[@]}"; do + echo "$(make_key "$entry")=global" >> "$STATE_FILE" +done +SKILLS_DRY_RUN=0 +install_selected >/dev/null + +assert_true "SKILL.md bundle copié" \ + "test -f '$TMP_HOME/.claude/skills/infra/bundle-demo/SKILL.md'" +assert_true "script bundle copié" \ + "test -f '$TMP_HOME/.claude/skills/infra/bundle-demo/scripts/check.sh'" +assert_true "template bundle copié" \ + "test -f '$TMP_HOME/.claude/skills/infra/bundle-demo/templates/report.md'" +assert_true "legacy copié en SKILL.md" \ + "test -f '$TMP_HOME/.claude/skills/dev/legacy-demo/SKILL.md'" + +rm -rf "$TMP_REPO" "$TMP_HOME" +rm -f "$STATE_FILE" +HOME="$OLD_HOME" + +echo "" +echo "══════════════════════════════════" +printf " Résultats : %d passés, %d échoués\n" "$PASS" "$FAIL" +echo "══════════════════════════════════" +echo "" +[[ "$FAIL" -eq 0 ]]