bug skill.md

This commit is contained in:
2026-05-16 06:59:49 +02:00
parent 3fce51d7f5
commit 2fe62335c4
3 changed files with 1833 additions and 0 deletions
+750
View File
@@ -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/<categorie>/<nom>/
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
<agent>.md -> ~/.<agent>/skills/<categorie>/<nom>/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/<categorie>/<nom>/
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 `<agent>.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 `<agent>.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 `<agent>.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/<categorie>/<nom>/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/<categorie>/<nom>/<agent>.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/<categorie>/<nom>/SKILL.md
skills/<categorie>/<nom>/scripts/
skills/<categorie>/<nom>/templates/
skills/<categorie>/<nom>/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 `<agent>.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=<dossier du skill>`.
### 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
<destination>/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
<dest_dir>/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 <agent>.md vers <destination>/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
<source_path>/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 :
- `<agent>.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/<categorie>/<nom>/SKILL.md
Le format historique <agent>.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 `<agent>.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.
Executable
+914
View File
@@ -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/tty
[[ "$answer" != "o" && "$answer" != "O" ]] && err "fzf requis. Abandon." && exit 1
if command -v apt-get &>/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 <agent>.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 "$@"
+169
View File
@@ -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 ]]