bug skill.md
This commit is contained in:
+750
@@ -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
@@ -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 "$@"
|
||||
@@ -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 ]]
|
||||
Reference in New Issue
Block a user