Compare commits
27 Commits
2107eb829f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f1f11d429 | |||
| 648184da43 | |||
| 358b0f5289 | |||
| 75d833d123 | |||
| bdf635e547 | |||
| 2fe62335c4 | |||
| 3fce51d7f5 | |||
| 22f79d68f0 | |||
| d131eeec5d | |||
| bd1ddd8193 | |||
| a2b592e885 | |||
| 548a4627f9 | |||
| f17a4e976a | |||
| 893957703a | |||
| 4dde0a9d8b | |||
| 25d0cfb0cb | |||
| 5088ec0189 | |||
| 436578968e | |||
| f40ddcb889 | |||
| 22b6b9b596 | |||
| 7b9fb3a231 | |||
| 00e7057708 | |||
| ef0b16879f | |||
| 4272da744c | |||
| 64fedcada3 | |||
| e3b9a7f59a | |||
| 8a38aec0a0 |
+1
-2
@@ -2,8 +2,7 @@
|
|||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
# Fichiers de travail personnels (notes, brouillons)
|
# Fichiers de travail personnels (notes, brouillons)
|
||||||
claude_code_skills_installer_guidelines_md.md
|
|
||||||
future-list_skill.md
|
|
||||||
|
|
||||||
# Fichiers temporaires système
|
# Fichiers temporaires système
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
+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.
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Consignes Claude Code — Système d’installation de Skills
|
||||||
|
|
||||||
|
tu est un expert senior en devellopement de script bash pour linux (debian ou ubuntu)
|
||||||
|
tu es egalement un expret en creation de skill pour des agents ia : claude code, codex, gemini cli, hermes agent et tu connais bien la structure du skill et des dossiers attendu
|
||||||
|
|
||||||
|
tu t'exprimera avec moi en francais comprehensible par un novice.
|
||||||
|
tes commentaires de code seront egalement en francais
|
||||||
|
|
||||||
|
tu analysera quels outils sont les plus utile pour develloper mon projet
|
||||||
|
tu deploiera un plan de devellopement avant toute ecriture de code
|
||||||
|
|
||||||
|
## Objectif général
|
||||||
|
|
||||||
|
Développer un système simple, autonome et robuste permettant :
|
||||||
|
- un repo comme base de mes skills_perso
|
||||||
|
- d’installer des skills IA via une commande shell ;
|
||||||
|
- d’activer/désactiver des skills ;
|
||||||
|
- de gérer les dépendances minimales ;
|
||||||
|
- de fonctionner principalement avec :
|
||||||
|
- `bash`
|
||||||
|
- `curl`
|
||||||
|
- `wget`
|
||||||
|
- `git`
|
||||||
|
|
||||||
|
|
||||||
|
Le système doit être pensé pour Debian/Linux principalement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Philosophie du projet
|
||||||
|
|
||||||
|
Le système doit être :
|
||||||
|
|
||||||
|
- minimaliste ;
|
||||||
|
- autonome ;
|
||||||
|
- facilement portable ;
|
||||||
|
- lisible ;
|
||||||
|
- maintenable ;
|
||||||
|
- compatible self-hosted ;
|
||||||
|
- orienté CLI ;
|
||||||
|
- compatible avec des agents IA locaux.
|
||||||
|
|
||||||
|
Éviter :
|
||||||
|
|
||||||
|
- les frameworks lourds ;
|
||||||
|
- les installateurs complexes ;
|
||||||
|
- les dépendances Python inutiles ;
|
||||||
|
- Node.js ;
|
||||||
|
- Docker ;
|
||||||
|
- les services système complexes.
|
||||||
|
|
||||||
|
Le système doit pouvoir fonctionner sur :
|
||||||
|
|
||||||
|
- Debian ;
|
||||||
|
- Ubuntu ;
|
||||||
|
- Proxmox ;
|
||||||
|
- VM Linux légères ;
|
||||||
|
- mini-PC ;
|
||||||
|
- environnements homelab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fonctionnalités attendues
|
||||||
|
|
||||||
|
|
||||||
|
un repo perso de stockage de mes skills perso.
|
||||||
|
tu definira la structure des dossier affin d'avoir un classement des skill judicieux, evolutif
|
||||||
|
|
||||||
|
## Installation de skills sur le poste client
|
||||||
|
|
||||||
|
Exemple attendu :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://gitea.maison43.duckdns.org/gilles/mes_skills/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
tu fera un brainstorming sur la meilleur outils pour generer ce script d'installation
|
||||||
|
|
||||||
|
Voici mes 1ere investigation ; rien n'est obligatoire.
|
||||||
|
Le système doit :
|
||||||
|
- un script bash
|
||||||
|
- installer uniquement les dépendances nécessaires ;
|
||||||
|
- detecter quels agent ia est installe sur le systeme
|
||||||
|
- detecter quels skills sont deja installer en global
|
||||||
|
- cloner ou copier le dépôt de skill dans un repertoire temporaire;
|
||||||
|
- demander pour quels agent je dois installer
|
||||||
|
- demander si installer en global ou dans dossier projet ( dossier actuel ou je lance la commande)
|
||||||
|
- proposer un menu avec la liste des skills installable/deja installe, a mettre a jours
|
||||||
|
- selectionner les skils via clavier felche haut, bas; space, esc, enter
|
||||||
|
- gerer un versioning des skill
|
||||||
|
- detecter de nouveaux skill intalle en local et non present dans mon repo ( possibilite d'uploader vers mon repo git un new skill)
|
||||||
|
|
||||||
|
|
||||||
|
tu pourra verifier sur les site suivant la structure d'un skill:
|
||||||
|
- https://code.claude.com/docs/fr/skills
|
||||||
|
- https://developers.openai.com/codex/skills
|
||||||
|
- https://geminicli.com/docs/cli/skills/
|
||||||
|
- https://hermes-agent.nousresearch.com/docs/guides/work-with-skills
|
||||||
|
|
||||||
|
## Suggestion
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
https://github.com/NousResearch/hermes-agent/blob/main/scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
analyse se script, sa mecanique me semble correspondre a ce que je veux
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Détection automatique
|
||||||
|
|
||||||
|
Le script doit détecter automatiquement :
|
||||||
|
|
||||||
|
- présence de `git`
|
||||||
|
- présence de `curl`
|
||||||
|
- présence de `wget`
|
||||||
|
- présence de `python3`
|
||||||
|
- présence de `uv`
|
||||||
|
- présence de `docker`
|
||||||
|
- présence de `podman`
|
||||||
|
|
||||||
|
Puis adapter l’installation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gestion Python
|
||||||
|
|
||||||
|
Si un skill nécessite Python :
|
||||||
|
|
||||||
|
Préférer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv
|
||||||
|
```
|
||||||
|
|
||||||
|
ou idéalement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
Éviter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install globalement
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque skill doit idéalement avoir son environnement isolé.
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# en vrac, liste de skill a creer
|
||||||
|
|
||||||
|
- home assistant : debug log, automatisation, dashboard optimization, service, entitie
|
||||||
|
- esphome ; creation, debuggazge amelioration
|
||||||
|
- esp32 : iot vias esp32, analyse puce et gpio, platformio, webserver, wifi,ap, ds18b20, dht22, relay, switch, rules, ota, ethernet, carte kincony
|
||||||
|
- traducteur de page markdown ( ne pas traduire les lien ), screen, touchscreen
|
||||||
|
- docker compose expert
|
||||||
|
- analyse reseau
|
||||||
|
- proxmox
|
||||||
|
- opensense expert
|
||||||
|
- serveur dns api
|
||||||
|
- cura printer
|
||||||
|
- bash expert
|
||||||
|
- rpi expert for iot
|
||||||
|
- nodered writer
|
||||||
|
- linux hardware optimisation
|
||||||
|
- selhosted homelab
|
||||||
|
- tuto ia ; ollama, hermes claude code, gemini cli, codex
|
||||||
|
- web page designer for laptop/smartphone
|
||||||
|
- matrix selfhosted expert (synapse)
|
||||||
|
- python coding expert, rust
|
||||||
|
- zigbee expert
|
||||||
|
- weewx expert ( merge old database)
|
||||||
|
- mqtt optimizer
|
||||||
|
- coral et expert image analyser
|
||||||
|
- create plan de dev pour un app selhosted
|
||||||
|
- debian et ubuntu optimizer
|
||||||
|
- asus tuf gaming (recherche internet evolution compatibilite avec linux)
|
||||||
|
- ia selfhosted ollazma, lmstudio cpu, nvidia, intel arc
|
||||||
|
- expert jardinnage
|
||||||
|
- expert diy et iot (recherche web)
|
||||||
|
- specialiste meteo ( recupere info meteo france)
|
||||||
+505
-98
@@ -4,16 +4,17 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Couleurs Gruvbox Dark 256 ──────────────────────────────────────
|
# ── Couleurs Gruvbox Dark 256 ──────────────────────────────────────
|
||||||
GRV_FG='\033[38;5;223m'
|
# $'\033' = vrai octet ESC (0x1B) — fonctionne dans tableaux bash et printf sans echo -e
|
||||||
GRV_RED='\033[38;5;167m'
|
GRV_FG=$'\033[38;5;223m'
|
||||||
GRV_GREEN='\033[38;5;142m'
|
GRV_RED=$'\033[38;5;167m'
|
||||||
GRV_YELLOW='\033[38;5;214m'
|
GRV_GREEN=$'\033[38;5;142m'
|
||||||
GRV_BLUE='\033[38;5;109m'
|
GRV_YELLOW=$'\033[38;5;214m'
|
||||||
GRV_PURPLE='\033[38;5;175m'
|
GRV_BLUE=$'\033[38;5;109m'
|
||||||
GRV_AQUA='\033[38;5;108m'
|
GRV_PURPLE=$'\033[38;5;175m'
|
||||||
GRV_ORANGE='\033[38;5;208m'
|
GRV_AQUA=$'\033[38;5;108m'
|
||||||
GRV_GRAY='\033[38;5;245m'
|
GRV_ORANGE=$'\033[38;5;208m'
|
||||||
RESET='\033[0m'
|
GRV_GRAY=$'\033[38;5;245m'
|
||||||
|
RESET=$'\033[0m'
|
||||||
|
|
||||||
# ── Thème fzf Gruvbox Dark ────────────────────────────────────────
|
# ── Thème fzf Gruvbox Dark ────────────────────────────────────────
|
||||||
export FZF_DEFAULT_OPTS="
|
export FZF_DEFAULT_OPTS="
|
||||||
@@ -21,7 +22,6 @@ export FZF_DEFAULT_OPTS="
|
|||||||
--color=fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934
|
--color=fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934
|
||||||
--color=marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934
|
--color=marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934
|
||||||
--border=rounded --height=80% --layout=reverse
|
--border=rounded --height=80% --layout=reverse
|
||||||
--header-lines=2
|
|
||||||
"
|
"
|
||||||
|
|
||||||
# ── Icônes ────────────────────────────────────────────────────────
|
# ── Icônes ────────────────────────────────────────────────────────
|
||||||
@@ -38,6 +38,7 @@ REPO_URL="https://gitea.maison43.duckdns.org/gilles/mes_skills.git"
|
|||||||
REPO_DIR="/tmp/mes_skills_$$"
|
REPO_DIR="/tmp/mes_skills_$$"
|
||||||
_CLONED_REPO_DIR=""
|
_CLONED_REPO_DIR=""
|
||||||
STATE_FILE="/tmp/skills_state_$$"
|
STATE_FILE="/tmp/skills_state_$$"
|
||||||
|
COLLAPSED_FILE="/tmp/skills_collapsed_$$"
|
||||||
|
|
||||||
SKILLS_DEBUG="${SKILLS_DEBUG:-0}"
|
SKILLS_DEBUG="${SKILLS_DEBUG:-0}"
|
||||||
SKILLS_DRY_RUN="${SKILLS_DRY_RUN:-0}"
|
SKILLS_DRY_RUN="${SKILLS_DRY_RUN:-0}"
|
||||||
@@ -58,14 +59,21 @@ cleanup() {
|
|||||||
debug "Nettoyage $_CLONED_REPO_DIR et $STATE_FILE"
|
debug "Nettoyage $_CLONED_REPO_DIR et $STATE_FILE"
|
||||||
[[ -n "$_CLONED_REPO_DIR" && -d "$_CLONED_REPO_DIR" ]] && rm -rf "$_CLONED_REPO_DIR"
|
[[ -n "$_CLONED_REPO_DIR" && -d "$_CLONED_REPO_DIR" ]] && rm -rf "$_CLONED_REPO_DIR"
|
||||||
[[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE"
|
[[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE"
|
||||||
|
[[ -f "$COLLAPSED_FILE" ]] && rm -f "$COLLAPSED_FILE"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
# ── Installation fzf ──────────────────────────────────────────────
|
# ── Installation fzf ──────────────────────────────────────────────
|
||||||
_install_fzf_binary() {
|
_install_fzf_binary() {
|
||||||
local fzf_url="https://github.com/junegunn/fzf/releases/latest/download/fzf-linux_amd64.tar.gz"
|
|
||||||
local tmp_fzf="/tmp/fzf_$$.tar.gz"
|
local tmp_fzf="/tmp/fzf_$$.tar.gz"
|
||||||
info "Téléchargement fzf depuis GitHub Releases..."
|
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"
|
curl -fsSL "$fzf_url" -o "$tmp_fzf"
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
tar -xzf "$tmp_fzf" -C "$HOME/.local/bin/" fzf
|
tar -xzf "$tmp_fzf" -C "$HOME/.local/bin/" fzf
|
||||||
@@ -76,7 +84,7 @@ _install_fzf_binary() {
|
|||||||
install_fzf() {
|
install_fzf() {
|
||||||
warn "fzf non trouvé."
|
warn "fzf non trouvé."
|
||||||
echo -e " ${GRV_FG}Installer fzf ? [o/N]${RESET} \c"
|
echo -e " ${GRV_FG}Installer fzf ? [o/N]${RESET} \c"
|
||||||
read -r answer
|
read -r answer </dev/tty
|
||||||
[[ "$answer" != "o" && "$answer" != "O" ]] && err "fzf requis. Abandon." && exit 1
|
[[ "$answer" != "o" && "$answer" != "O" ]] && err "fzf requis. Abandon." && exit 1
|
||||||
if command -v apt-get &>/dev/null; then
|
if command -v apt-get &>/dev/null; then
|
||||||
debug "Installation via apt"
|
debug "Installation via apt"
|
||||||
@@ -119,36 +127,40 @@ detect_agents() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_detect_gemini() {
|
# Cherche un binaire dans les répertoires courants : PATH, npm-global, nvm, local
|
||||||
command -v gemini &>/dev/null && return 0
|
_find_bin() {
|
||||||
local prefix
|
local bin="$1"
|
||||||
prefix=$(npm config get prefix 2>/dev/null) || return 1
|
command -v "$bin" &>/dev/null && return 0
|
||||||
[[ -n "$prefix" && -f "${prefix}/bin/gemini" ]]
|
[[ -f "$HOME/.npm-global/bin/$bin" ]] && return 0
|
||||||
|
[[ -f "$HOME/.local/bin/$bin" ]] && return 0
|
||||||
|
# nvm : toutes les versions node installées
|
||||||
|
find "$HOME/.nvm/versions/node" -name "$bin" -path "*/bin/$bin" 2>/dev/null | grep -q . && return 0
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# claude-code
|
# claude-code
|
||||||
if [[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null; then
|
if [[ -d "$HOME/.claude" ]] || _find_bin "claude"; then
|
||||||
_add_agent "claude-code"
|
_add_agent "claude-code"
|
||||||
else
|
else
|
||||||
_skip_agent "claude-code"
|
_skip_agent "claude-code"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# gemini-cli
|
# gemini-cli
|
||||||
if _detect_gemini; then
|
if _find_bin "gemini"; then
|
||||||
_add_agent "gemini-cli"
|
_add_agent "gemini-cli"
|
||||||
else
|
else
|
||||||
_skip_agent "gemini-cli"
|
_skip_agent "gemini-cli"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# codex
|
# codex
|
||||||
if command -v codex &>/dev/null || [[ -f "$HOME/.npm-global/bin/codex" ]]; then
|
if _find_bin "codex"; then
|
||||||
_add_agent "codex"
|
_add_agent "codex"
|
||||||
else
|
else
|
||||||
_skip_agent "codex"
|
_skip_agent "codex"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# hermes
|
# hermes
|
||||||
if command -v hermes &>/dev/null || [[ -f "$HOME/.local/bin/hermes" ]]; then
|
if _find_bin "hermes"; then
|
||||||
_add_agent "hermes"
|
_add_agent "hermes"
|
||||||
else
|
else
|
||||||
_skip_agent "hermes"
|
_skip_agent "hermes"
|
||||||
@@ -159,6 +171,43 @@ detect_agents() {
|
|||||||
fi
|
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 du dépôt ────────────────────────────────────────────────
|
||||||
clone_repo() {
|
clone_repo() {
|
||||||
header "Récupération du dépôt"
|
header "Récupération du dépôt"
|
||||||
@@ -179,6 +228,22 @@ get_frontmatter_field() {
|
|||||||
grep "^${field}:" "$file" 2>/dev/null | head -1 | awk '{print $2}' | tr -d "\"'"
|
grep "^${field}:" "$file" 2>/dev/null | head -1 | awk '{print $2}' | tr -d "\"'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_frontmatter_desc() {
|
||||||
|
grep "^description:" "$1" 2>/dev/null | head -1 \
|
||||||
|
| sed 's/^description:[[:space:]]*//' | 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/^/#/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse "agents: [claude-code, codex]" → une ligne par agent
|
||||||
|
get_frontmatter_agents() {
|
||||||
|
grep "^agents:" "$1" 2>/dev/null | head -1 \
|
||||||
|
| sed 's/^agents:[[:space:]]*//' | tr -d '[]' | tr ',' '\n' | tr -d ' ' | grep -v '^$'
|
||||||
|
}
|
||||||
|
|
||||||
# Retourne 0 (succès) si ver2 est plus récente que ver1
|
# Retourne 0 (succès) si ver2 est plus récente que ver1
|
||||||
version_is_newer() {
|
version_is_newer() {
|
||||||
local ver1="$1" ver2="$2"
|
local ver1="$1" ver2="$2"
|
||||||
@@ -209,13 +274,65 @@ get_local_version() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Scan des skills disponibles ───────────────────────────────────
|
# ── Scan des skills disponibles ───────────────────────────────────
|
||||||
# Format : "cat|skill|agent|etat|repo_version|local_version"
|
# Format : "cat|skill|agent|etat|repo_ver|local_ver|desc|tags|kind|source_path"
|
||||||
|
# kind=bundle → dossier SKILL.md+ressources | kind=legacy → fichier <agent>.md
|
||||||
SKILLS_LIST=()
|
SKILLS_LIST=()
|
||||||
|
|
||||||
|
_skill_agent_ok() {
|
||||||
|
local agent="$1"
|
||||||
|
[[ ${#DETECTED_AGENTS[@]} -eq 0 ]] && return 0
|
||||||
|
for a in "${DETECTED_AGENTS[@]}"; do [[ "$a" == "$agent" ]] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_build_entry() {
|
||||||
|
local cat="$1" skill="$2" agent="$3" kind="$4" source_path="$5" skill_file="$6"
|
||||||
|
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")
|
||||||
|
echo "${cat}|${skill}|${agent}|${etat}|${repo_ver}|${local_ver}|${desc}|${tags}|${kind}|${source_path}"
|
||||||
|
}
|
||||||
|
|
||||||
scan_skills() {
|
scan_skills() {
|
||||||
header "Scan des skills disponibles"
|
header "Scan des skills disponibles"
|
||||||
SKILLS_LIST=()
|
SKILLS_LIST=()
|
||||||
|
declare -A seen_combos
|
||||||
|
|
||||||
|
# ── Chemin 1 : bundles (dossier avec SKILL.md, agents: dans le frontmatter)
|
||||||
|
while IFS= read -r skill_file; do
|
||||||
|
local rel="${skill_file#${REPO_DIR}/skills/}"
|
||||||
|
local skill_dir="${rel%/SKILL.md}"
|
||||||
|
local cat="${skill_dir%%/*}"
|
||||||
|
local skill="${skill_dir#*/}"
|
||||||
|
local source_dir="${REPO_DIR}/skills/${cat}/${skill}"
|
||||||
|
|
||||||
|
local raw_agents; raw_agents=$(get_frontmatter_agents "$skill_file")
|
||||||
|
if [[ -z "$raw_agents" ]]; then
|
||||||
|
debug "Bundle sans agents: ignoré : $cat/$skill"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r agent; do
|
||||||
|
_skill_agent_ok "$agent" || continue
|
||||||
|
[[ -n "$SKILLS_TAG" ]] && { grep -q "$SKILLS_TAG" "$skill_file" || continue; }
|
||||||
|
local entry; entry=$(_build_entry "$cat" "$skill" "$agent" "bundle" "$source_dir" "$skill_file")
|
||||||
|
SKILLS_LIST+=("$entry")
|
||||||
|
seen_combos["${cat}|${skill}|${agent}"]=1
|
||||||
|
debug "Bundle : $cat/$skill [$agent]"
|
||||||
|
done <<< "$raw_agents"
|
||||||
|
done < <(find "${REPO_DIR}/skills" -name "SKILL.md" | sort)
|
||||||
|
|
||||||
|
# ── Chemin 2 : legacy (<agent>.md — agents connus uniquement)
|
||||||
|
local known_agents="claude-code gemini-cli codex hermes"
|
||||||
while IFS= read -r skill_file; do
|
while IFS= read -r skill_file; do
|
||||||
local rel="${skill_file#${REPO_DIR}/skills/}"
|
local rel="${skill_file#${REPO_DIR}/skills/}"
|
||||||
local agent_file="${rel##*/}"
|
local agent_file="${rel##*/}"
|
||||||
@@ -224,35 +341,21 @@ scan_skills() {
|
|||||||
local cat="${skill_path%%/*}"
|
local cat="${skill_path%%/*}"
|
||||||
local skill="${skill_path#*/}"
|
local skill="${skill_path#*/}"
|
||||||
|
|
||||||
# Filtre agent
|
# Ignorer les fichiers qui ne correspondent pas à un agent connu
|
||||||
local agent_detected=0
|
local is_known=0
|
||||||
if [[ ${#DETECTED_AGENTS[@]} -gt 0 ]]; then
|
for ka in $known_agents; do [[ "$ka" == "$agent" ]] && is_known=1; done
|
||||||
for a in "${DETECTED_AGENTS[@]}"; do
|
[[ "$is_known" -eq 0 ]] && continue
|
||||||
[[ "$a" == "$agent" ]] && agent_detected=1
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
[[ "$agent_detected" -eq 0 && ${#DETECTED_AGENTS[@]} -gt 0 ]] && continue
|
|
||||||
|
|
||||||
# Filtre tag
|
# Le bundle a priorité : ignorer si déjà vu
|
||||||
if [[ -n "$SKILLS_TAG" ]]; then
|
[[ -n "${seen_combos[${cat}|${skill}|${agent}]:-}" ]] && continue
|
||||||
grep -q "$SKILLS_TAG" "$skill_file" || continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local repo_ver; repo_ver=$(get_frontmatter_field "$skill_file" "version")
|
_skill_agent_ok "$agent" || continue
|
||||||
local local_ver; local_ver=$(get_local_version "$cat" "$skill" "$agent")
|
[[ -n "$SKILLS_TAG" ]] && { grep -q "$SKILLS_TAG" "$skill_file" || continue; }
|
||||||
local etat
|
|
||||||
|
|
||||||
if [[ -z "$local_ver" ]]; then
|
local entry; entry=$(_build_entry "$cat" "$skill" "$agent" "legacy" "$skill_file" "$skill_file")
|
||||||
etat="new"
|
SKILLS_LIST+=("$entry")
|
||||||
elif version_is_newer "$local_ver" "$repo_ver"; then
|
debug "Legacy : $cat/$skill [$agent]"
|
||||||
etat="upd"
|
done < <(find "${REPO_DIR}/skills" -name "*.md" ! -name "SKILL.md" | sort)
|
||||||
else
|
|
||||||
etat="ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SKILLS_LIST+=("${cat}|${skill}|${agent}|${etat}|${repo_ver}|${local_ver}")
|
|
||||||
debug "Skill trouvé : $cat/$skill [$agent] état=$etat"
|
|
||||||
done < <(find "${REPO_DIR}/skills" -name "*.md" | sort)
|
|
||||||
|
|
||||||
ok "${#SKILLS_LIST[@]} skill(s) trouvé(s)"
|
ok "${#SKILLS_LIST[@]} skill(s) trouvé(s)"
|
||||||
}
|
}
|
||||||
@@ -294,11 +397,11 @@ state_cycle() {
|
|||||||
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
|
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Formatage ligne menu ──────────────────────────────────────────
|
# ── Formatage ligne skill (préfixe caché s:IDX\t pour fzf) ───────
|
||||||
format_skill_line() {
|
format_skill_line() {
|
||||||
local entry="$1"
|
local entry="$1" idx="$2"
|
||||||
local cat skill agent etat repo_ver local_ver
|
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 <<< "$entry"
|
IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags kind source_path <<< "$entry"
|
||||||
local key; key=$(make_key "$entry")
|
local key; key=$(make_key "$entry")
|
||||||
local action; action=$(state_get "$key")
|
local action; action=$(state_get "$key")
|
||||||
|
|
||||||
@@ -323,36 +426,75 @@ format_skill_line() {
|
|||||||
[[ "$etat" == "upd" ]] && ver_info=" (${local_ver}→${repo_ver})"
|
[[ "$etat" == "upd" ]] && ver_info=" (${local_ver}→${repo_ver})"
|
||||||
[[ "$etat" == "new" ]] && ver_info=" (v${repo_ver})"
|
[[ "$etat" == "new" ]] && ver_info=" (v${repo_ver})"
|
||||||
|
|
||||||
printf "${color_etat}%s${RESET} %-35s ${GRV_GRAY}[%s]${RESET} ${color_action}%s${RESET}%s\n" \
|
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" \
|
||||||
"$ico_etat" "${cat}/${skill}" "$agent" "$ico_action" "$ver_info"
|
"$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 ────────────────────────────────────────────
|
# ── Menu fzf principal ────────────────────────────────────────────
|
||||||
run_menu() {
|
run_menu() {
|
||||||
header "Sélection des skills"
|
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
|
state_init
|
||||||
|
|
||||||
local cycle_script="/tmp/skills_cycle_$$.sh"
|
# 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 list_script="/tmp/skills_list_$$.sh"
|
||||||
local fns_file="/tmp/skills_fns_$$.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"
|
||||||
|
|
||||||
# Script de cycle d'état (appelé par fzf via execute-silent)
|
# Exporter fonctions et données dans un fichier source partagé
|
||||||
cat > "$cycle_script" << 'CYCLE_EOF'
|
{
|
||||||
|
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
|
#!/usr/bin/env bash
|
||||||
STATE_FILE="$1"
|
source "FNSFILE"
|
||||||
SKILLS_FNS="$2"
|
td="$1"; type="${td%%:*}"; value="${td#*:}"
|
||||||
LINE_NUM="$3" # numéro de ligne fzf (1-based)
|
[[ "$type" != "s" ]] && exit 0
|
||||||
|
entry="${SKILLS_LIST[$value]:-}"
|
||||||
source "$SKILLS_FNS"
|
|
||||||
|
|
||||||
# Récupère l'entrée correspondante (0-based dans SKILLS_LIST)
|
|
||||||
idx=$(( LINE_NUM - 1 ))
|
|
||||||
entry="${SKILLS_LIST[$idx]:-}"
|
|
||||||
[[ -z "$entry" ]] && exit 0
|
[[ -z "$entry" ]] && exit 0
|
||||||
|
|
||||||
key=$(make_key "$entry")
|
key=$(make_key "$entry")
|
||||||
etat=$(echo "$entry" | cut -d'|' -f4)
|
etat=$(echo "$entry" | cut -d'|' -f4)
|
||||||
|
|
||||||
current=$(grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2)
|
current=$(grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2)
|
||||||
case "$current" in
|
case "$current" in
|
||||||
local) next="global" ;;
|
local) next="global" ;;
|
||||||
@@ -362,39 +504,294 @@ case "$current" in
|
|||||||
*) next="local" ;;
|
*) next="local" ;;
|
||||||
esac
|
esac
|
||||||
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
|
sed -i "s|^${key}=.*|${key}=${next}|" "$STATE_FILE"
|
||||||
CYCLE_EOF
|
SPACE_EOF
|
||||||
chmod +x "$cycle_script"
|
sed -i "s|FNSFILE|$fns_file|" "$space_script"
|
||||||
|
chmod +x "$space_script"
|
||||||
|
|
||||||
# Exporter fonctions et variables dans un fichier source
|
# Script X : plier/déplier une catégorie (anciennement TAB)
|
||||||
{
|
cat > "$fold_script" << 'FOLD_EOF'
|
||||||
declare -f format_skill_line state_get make_key
|
|
||||||
declare -p GRV_GREEN GRV_YELLOW GRV_AQUA GRV_GRAY GRV_BLUE RESET \
|
|
||||||
ICO_OK ICO_UPD ICO_NEW ICO_NA ICO_LOCAL ICO_GLOBAL ICO_SKIP
|
|
||||||
echo "STATE_FILE='$STATE_FILE'"
|
|
||||||
echo "SKILLS_LIST=($(printf '"%s" ' "${SKILLS_LIST[@]}"))"
|
|
||||||
} > "$fns_file"
|
|
||||||
|
|
||||||
# Script générateur de liste pour fzf --reload
|
|
||||||
cat > "$list_script" << LIST_EOF
|
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
source "$fns_file"
|
source "FNSFILE"
|
||||||
for entry in "\${SKILLS_LIST[@]}"; do
|
td="$1"; type="${td%%:*}"; value="${td#*:}"
|
||||||
format_skill_line "\$entry"
|
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
|
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=()
|
||||||
|
local -a agents_to_show=("${DETECTED_AGENTS[@]}")
|
||||||
|
[[ "${#agents_to_show[@]}" -eq 0 ]] && agents_to_show=("claude-code" "gemini-cli" "codex" "hermes")
|
||||||
|
for agent in "${agents_to_show[@]}"; do
|
||||||
|
base="${agent_dir_map[$agent]:-}"
|
||||||
|
[[ -z "$base" ]] && continue
|
||||||
|
while IFS= read -r skill_md; do
|
||||||
|
rel="${skill_md#${base}/skills/}"
|
||||||
|
local path_part="${rel%/SKILL.md}"
|
||||||
|
local slashes="${path_part//[^\/]/}"
|
||||||
|
local cat_name skill_name display
|
||||||
|
if [[ "${#slashes}" -ge 1 ]]; then
|
||||||
|
cat_name="${path_part%%/*}"
|
||||||
|
skill_name="${path_part#*/}"
|
||||||
|
display="${cat_name}/${skill_name}"
|
||||||
|
else
|
||||||
|
cat_name=""
|
||||||
|
skill_name="$path_part"
|
||||||
|
display="$skill_name"
|
||||||
|
fi
|
||||||
|
ver=$(grep "^version:" "$skill_md" 2>/dev/null | head -1 | awk '{print $2}')
|
||||||
|
global_lines+=("g:${agent}|${cat_name}|${skill_name}\t ${GRV_GRAY}${display}${RESET} ${GRV_GRAY}[${agent}]${RESET} ${GRV_GRAY}v${ver:-?}${RESET}")
|
||||||
|
done < <(find "${base}/skills" -name "SKILL.md" -not -path '*/.*' 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
|
LIST_EOF
|
||||||
|
sed -i "s|FNSFILE|$fns_file|" "$list_script"
|
||||||
chmod +x "$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
|
||||||
|
if [[ -n "$cat_name" ]]; then
|
||||||
|
skill_file="${base}/skills/${cat_name}/${skill_name}/SKILL.md"
|
||||||
|
else
|
||||||
|
skill_file="${base}/skills/${skill_name}/SKILL.md"
|
||||||
|
fi
|
||||||
|
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
|
local legend
|
||||||
legend=$(echo -e "${GRV_GRAY}État: ${GRV_GREEN}✓ installé ${GRV_YELLOW}↑ MAJ ${GRV_AQUA}+ nouveau Action: ${GRV_GREEN}●L local ${GRV_BLUE}●G global ${GRV_GRAY}○ ignorer TAB=changer ENTER=confirmer ESC=quitter${RESET}")
|
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 \
|
fzf \
|
||||||
--ansi \
|
--ansi \
|
||||||
|
--delimiter='\t' \
|
||||||
|
--with-nth=2.. \
|
||||||
|
--nth=2.. \
|
||||||
--prompt="Skills > " \
|
--prompt="Skills > " \
|
||||||
--header="$legend" \
|
--header="$legend" \
|
||||||
--bind="tab:execute-silent($cycle_script '$STATE_FILE' '$fns_file' {n})+reload($list_script)" \
|
--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
|
< <(bash "$list_script") > /dev/null || true
|
||||||
|
|
||||||
rm -f "$cycle_script" "$list_script" "$fns_file"
|
rm -f "$space_script" "$fold_script" "$tab_script" "$list_script" "$fns_file" "$preview_script" "$copy_script" "$help_file" "$mode_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Installation ──────────────────────────────────────────────────
|
# ── Installation ──────────────────────────────────────────────────
|
||||||
@@ -403,8 +800,8 @@ install_selected() {
|
|||||||
local count_install=0 count_update=0 count_skip=0
|
local count_install=0 count_update=0 count_skip=0
|
||||||
|
|
||||||
for entry in "${SKILLS_LIST[@]}"; do
|
for entry in "${SKILLS_LIST[@]}"; do
|
||||||
local cat skill agent etat repo_ver local_ver
|
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 <<< "$entry"
|
IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags kind source_path <<< "$entry"
|
||||||
local key; key=$(make_key "$entry")
|
local key; key=$(make_key "$entry")
|
||||||
local action; action=$(state_get "$key")
|
local action; action=$(state_get "$key")
|
||||||
|
|
||||||
@@ -416,16 +813,25 @@ install_selected() {
|
|||||||
local scope="$action"
|
local scope="$action"
|
||||||
[[ "$action" == "update" ]] && scope="local"
|
[[ "$action" == "update" ]] && scope="local"
|
||||||
|
|
||||||
local src="${REPO_DIR}/skills/${cat}/${skill}/${agent}.md"
|
|
||||||
local dest; dest=$(get_dest_path "$cat" "$skill" "$agent" "$scope")
|
local dest; dest=$(get_dest_path "$cat" "$skill" "$agent" "$scope")
|
||||||
|
|
||||||
debug "Copie $src → $dest"
|
if [[ "$kind" == "bundle" ]]; then
|
||||||
|
local dest_dir; dest_dir="$(dirname "$dest")"
|
||||||
|
debug "Bundle $source_path/ → $dest_dir/"
|
||||||
if [[ "$SKILLS_DRY_RUN" == "1" ]]; then
|
if [[ "$SKILLS_DRY_RUN" == "1" ]]; then
|
||||||
info "[DRY-RUN] cp $src → $dest"
|
info "[DRY-RUN] cp -r $source_path/ → $dest_dir/"
|
||||||
|
else
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
cp -r "${source_path}/." "$dest_dir/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
debug "Legacy $source_path → $dest"
|
||||||
|
if [[ "$SKILLS_DRY_RUN" == "1" ]]; then
|
||||||
|
info "[DRY-RUN] cp $source_path → $dest"
|
||||||
else
|
else
|
||||||
mkdir -p "$(dirname "$dest")"
|
mkdir -p "$(dirname "$dest")"
|
||||||
cp "$src" "$dest"
|
cp "$source_path" "$dest"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$action" == "update" ]]; then
|
if [[ "$action" == "update" ]]; then
|
||||||
@@ -449,8 +855,8 @@ print_summary() {
|
|||||||
|
|
||||||
echo -e "\n${GRV_PURPLE}╔══ Tester vos skills ══╗${RESET}\n"
|
echo -e "\n${GRV_PURPLE}╔══ Tester vos skills ══╗${RESET}\n"
|
||||||
for entry in "${SKILLS_LIST[@]}"; do
|
for entry in "${SKILLS_LIST[@]}"; do
|
||||||
local cat skill agent etat repo_ver local_ver
|
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 <<< "$entry"
|
IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags kind source_path <<< "$entry"
|
||||||
local key; key=$(make_key "$entry")
|
local key; key=$(make_key "$entry")
|
||||||
local action; action=$(state_get "$key")
|
local action; action=$(state_get "$key")
|
||||||
[[ "$action" == "skip" ]] && continue
|
[[ "$action" == "skip" ]] && continue
|
||||||
@@ -490,6 +896,7 @@ main() {
|
|||||||
|
|
||||||
check_deps
|
check_deps
|
||||||
detect_agents
|
detect_agents
|
||||||
|
select_agents
|
||||||
clone_repo
|
clone_repo
|
||||||
scan_skills
|
scan_skills
|
||||||
|
|
||||||
|
|||||||
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,28 @@
|
|||||||
|
---
|
||||||
|
name: git-expert
|
||||||
|
version: 1.0.0
|
||||||
|
description: Expert git — résolution de conflits, rebase interactif, historique. Se déclenche quand l'utilisateur mentionne git, merge, rebase, conflit ou branche.
|
||||||
|
agents: [claude-code]
|
||||||
|
category: dev
|
||||||
|
tags: [git, vcs, merge, rebase]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Expert
|
||||||
|
|
||||||
|
Tu es un expert git. Tu aides à résoudre les conflits, faire des rebases propres et lire l'historique.
|
||||||
|
|
||||||
|
## Quand utiliser ce skill
|
||||||
|
|
||||||
|
- Résolution de conflits de merge
|
||||||
|
- Rebase interactif (`git rebase -i`)
|
||||||
|
- Nettoyage d'historique
|
||||||
|
- Questions sur les branches
|
||||||
|
|
||||||
|
## Commandes essentielles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline --graph --all # Vue graphique de l'historique
|
||||||
|
git rebase -i HEAD~5 # Rebase interactif sur les 5 derniers commits
|
||||||
|
git stash && git pull && git stash pop # Pull sans perdre les modifs locales
|
||||||
|
git bisect start # Trouver le commit qui a introduit un bug
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: docker-compose
|
||||||
|
version: 1.0.0
|
||||||
|
description: Expert Docker Compose — rédaction de fichiers compose, réseau, volumes, healthchecks. Se déclenche sur docker-compose, conteneur, service, image.
|
||||||
|
agents: [claude-code]
|
||||||
|
category: infra
|
||||||
|
tags: [docker, compose, container, homelab]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker Compose Expert
|
||||||
|
|
||||||
|
Tu maîtrises Docker Compose v2 pour des environnements homelab et production légère.
|
||||||
|
|
||||||
|
## Quand utiliser ce skill
|
||||||
|
|
||||||
|
- Écriture ou correction d'un `docker-compose.yml`
|
||||||
|
- Problèmes de réseau entre conteneurs
|
||||||
|
- Configuration de volumes et secrets
|
||||||
|
- Healthchecks et restart policies
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: mon-image:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
name: ha-log-investigator
|
||||||
|
description: Diagnostic Home Assistant en français pour auditer une installation, vérifier la version installée, inventorier le matériel, analyser les logs, les dashboards, les entités et les fichiers YAML, puis produire repair.md, best_entity.md et memory.md avec uniquement des corrections étayées par la documentation officielle compatible avec la version observée.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HA Log Investigator
|
||||||
|
|
||||||
|
## Règles non négociables
|
||||||
|
|
||||||
|
- Répondre en français.
|
||||||
|
- Vérifier la version exacte de Home Assistant avant toute analyse ou recommandation. Si elle est inconnue, arrêter l'analyse et demander la preuve minimale nécessaire.
|
||||||
|
- Ne jamais inventer une correction. Ne proposer une action que si elle est confirmée par une source officielle Home Assistant compatible avec la version installée.
|
||||||
|
- Distinguer clairement : **constaté**, **probable**, **à vérifier**.
|
||||||
|
- Préserver les secrets : ne jamais afficher de mot de passe ou token dans les sorties, les logs ou les fichiers de rapport.
|
||||||
|
- Ne pas modifier l'installation sans demande explicite de l'utilisateur.
|
||||||
|
- Avant toute suppression demandée d'entité, service, automatisation, script ou élément de dashboard, créer une trace de retour arrière selon `references/change-trace.md`.
|
||||||
|
|
||||||
|
## Workflow obligatoire
|
||||||
|
|
||||||
|
1. **Établir le périmètre et la version**
|
||||||
|
- Identifier le mode d'installation si possible : Home Assistant OS, Container, Core, Supervised.
|
||||||
|
- Obtenir la version exacte de Home Assistant Core avant tout diagnostic.
|
||||||
|
- Consigner la source de la version et l'horodatage de collecte.
|
||||||
|
|
||||||
|
2. **Choisir le mode d'accès minimal suffisant**
|
||||||
|
- Commencer par les artefacts fournis localement : exports de logs, `configuration.yaml`, autres fichiers YAML, exports de dashboards, diagnostics d'intégration.
|
||||||
|
- N'utiliser SSH que si les artefacts fournis ne suffisent pas pour lire les métriques hôte, les logs système, ou des fichiers absents.
|
||||||
|
- Ne pas exiger de serveur MCP spécifique par défaut. Utiliser un MCP uniquement s'il existe déjà dans l'environnement et apporte un accès plus sûr ou plus structuré.
|
||||||
|
- Lire `references/access-and-evidence.md` avant de choisir un accès distant.
|
||||||
|
|
||||||
|
3. **Constituer la synthèse système**
|
||||||
|
- Produire un résumé matériel : CPU, mémoire totale, mémoire utilisée/libre, swap si disponible, espace disque total/utilisé/libre par volume pertinent.
|
||||||
|
- Mentionner la provenance des mesures et signaler toute donnée indisponible.
|
||||||
|
|
||||||
|
4. **Analyser Home Assistant**
|
||||||
|
- Déterminer d'abord le type d'installation, puis examiner la source de logs officielle adaptée : sur Home Assistant OS, privilégier l'UI `Settings > System > Logs` ou `ha core logs` si autorisé ; ne traiter `/config/home-assistant.log` comme source attendue que si un doublon de fichier a été activé.
|
||||||
|
- Examiner ensuite les journaux disponibles, les traces fournies et les diagnostics.
|
||||||
|
- Si `rtk` est disponible localement, générer en plus un résumé compact des logs avec `rtk log` sans supprimer la source brute.
|
||||||
|
- Détecter les intégrations fautives : erreurs répétées, échecs d'initialisation, timeouts, authentification, dépendances manquantes, dépréciations.
|
||||||
|
- Identifier les entités problématiques : indisponibles, orphelines, dupliquées, mal nommées, sources de spam de logs, historiques incohérents.
|
||||||
|
- Analyser les dashboards : erreurs de cartes, ressources manquantes, références à des entités absentes, vues invalides, YAML Lovelace ou stockage `.storage` si disponible.
|
||||||
|
- Analyser `configuration.yaml` et les fichiers inclus ; si l'installation fournit un `config.yaml`, l'analyser aussi. Relever erreurs de syntaxe, inclusions fragiles, redondances et opportunités de simplification.
|
||||||
|
- Si Watchman est installé, lire son rapport final et l'utiliser comme source complémentaire pour les entités/actions référencées mais manquantes.
|
||||||
|
- Si Spook est installé, lire ses réparations et utiliser ses actions de suivi d'issues comme complément pour marquer, créer ou gérer des problèmes visibles dans le dashboard Repairs.
|
||||||
|
|
||||||
|
5. **Valider les corrections**
|
||||||
|
- Vérifier chaque proposition contre la documentation officielle Home Assistant correspondant à la version installée.
|
||||||
|
- Si la documentation officielle ne couvre pas le cas, écrire explicitement : `Aucune correction certaine sans documentation officielle compatible` et proposer seulement des vérifications complémentaires.
|
||||||
|
- Lire `references/official-docs-policy.md` avant de rédiger les recommandations.
|
||||||
|
|
||||||
|
6. **Produire les livrables**
|
||||||
|
- Générer `repair.md` en suivant `assets/repair.md.template`.
|
||||||
|
- Générer `best_entity.md` en suivant `assets/best_entity.md.template`.
|
||||||
|
- Générer ou mettre à jour `memory.md` en suivant `assets/memory.md.template`, en n'y conservant que les informations durables et non sensibles utiles aux audits futurs.
|
||||||
|
- Si l'utilisateur fournit des accès, les enregistrer dans `.ha-log-investigator/credentials.env` à partir de `assets/credentials.env.template`, avec permissions restrictives, puis masquer les valeurs dans tous les rapports.
|
||||||
|
- Pour SSH, accepter l'hôte, le port, l'utilisateur et soit un mot de passe soit un chemin de clé privée. Préférer la clé SSH quand elle existe, sans refuser le mot de passe si c'est le seul mode disponible.
|
||||||
|
- Sur Home Assistant OS avec plusieurs add-ons SSH, utiliser de préférence un profil dédié à l'add-on officiel `Terminal & SSH` pour les logs Core et tracer quel profil a servi à chaque collecte.
|
||||||
|
|
||||||
|
## Contenu attendu de l'analyse
|
||||||
|
|
||||||
|
### `repair.md`
|
||||||
|
Inclure au minimum :
|
||||||
|
- version Home Assistant vérifiée ;
|
||||||
|
- synthèse matériel ;
|
||||||
|
- sources analysées ;
|
||||||
|
- problèmes classés par sévérité ;
|
||||||
|
- intégrations fautives ;
|
||||||
|
- erreurs dashboards ;
|
||||||
|
- entités problématiques ;
|
||||||
|
- anomalies de configuration ;
|
||||||
|
- corrections proposées, preuve officielle, compatibilité de version, niveau de confiance ;
|
||||||
|
- points non résolus et données manquantes.
|
||||||
|
|
||||||
|
### `best_entity.md`
|
||||||
|
Inclure au minimum :
|
||||||
|
- entités à renommer ou normaliser ;
|
||||||
|
- entités inutilisées, dupliquées ou trop bavardes ;
|
||||||
|
- suggestions d'amélioration pour automatisations, scripts, helpers et configuration ;
|
||||||
|
- opportunités de regroupement, templates, zones, labels ou dashboards ;
|
||||||
|
- bénéfice attendu et effort estimé ;
|
||||||
|
- uniquement des suggestions compatibles avec la version vérifiée.
|
||||||
|
|
||||||
|
|
||||||
|
### `memory.md`
|
||||||
|
Inclure au minimum :
|
||||||
|
- identité durable de l'installation ;
|
||||||
|
- architecture et matériel stable ;
|
||||||
|
- modes d'accès disponibles sans exposer de secret ;
|
||||||
|
- emplacements de fichiers importants ;
|
||||||
|
- intégrations majeures ;
|
||||||
|
- conventions locales ;
|
||||||
|
- problèmes connus, décisions de maintenance et points à revoir au prochain audit.
|
||||||
|
|
||||||
|
Ne jamais y enregistrer de mot de passe, token ou clé privée.
|
||||||
|
|
||||||
|
|
||||||
|
## Mise à jour du skill
|
||||||
|
|
||||||
|
Si une évolution officielle de Home Assistant est détectée pendant un audit :
|
||||||
|
1. vérifier l'évolution dans la documentation officielle ;
|
||||||
|
2. suivre `references/skill-update.md` ;
|
||||||
|
3. mettre à jour les instructions/scripts nécessaires ;
|
||||||
|
4. ajouter l'entrée correspondante dans `history.md` ;
|
||||||
|
5. revalider puis retester le skill si possible.
|
||||||
|
|
||||||
|
## Références et gabarits
|
||||||
|
|
||||||
|
- Lire `references/access-and-evidence.md` pour choisir l'accès minimal et savoir quelles preuves collecter.
|
||||||
|
- Lire `references/official-docs-policy.md` pour la hiérarchie des sources et la règle de non-invention.
|
||||||
|
- Lire `references/log-analysis.md` pour la collecte, le compactage `rtk` et le traitement des logs absents.
|
||||||
|
- Lire `references/change-trace.md` avant toute suppression demandée.
|
||||||
|
- Lire `references/ssh-collector.md` avant d'utiliser le collecteur SSH.
|
||||||
|
- Lire `references/watchman.md` si l'intégration Watchman est installée ou si un rapport `watchman_report.txt` est présent.
|
||||||
|
- Lire `references/spook.md` si l'intégration Spook est installée ou si l'utilisateur veut exploiter les repairs / issues gérées par Spook.
|
||||||
|
- Lire `references/skill-update.md` lorsqu'une évolution officielle de Home Assistant semble rendre une procédure du skill obsolète.
|
||||||
|
- Mettre à jour `history.md` à chaque évolution confirmée du skill.
|
||||||
|
- Utiliser `scripts/collect_ssh_evidence.sh` lorsqu'une collecte SSH reproductible en lecture seule est utile.
|
||||||
|
- Copier puis compléter `assets/repair.md.template`, `assets/best_entity.md.template` et `assets/memory.md.template` pour les livrables.
|
||||||
|
- Copier `assets/credentials.env.template` seulement si l'utilisateur fournit réellement des identifiants ou un token.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "HA Log Investigator"
|
||||||
|
short_description: "Diagnostic Home Assistant en français"
|
||||||
|
default_prompt: "Utilise ha-log-investigator pour auditer mon installation Home Assistant, analyser les logs et produire repair.md et best_entity.md."
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# best_entity.md
|
||||||
|
|
||||||
|
## 1. Principes d'amélioration
|
||||||
|
|
||||||
|
## 2. Entités à améliorer
|
||||||
|
| Entité | Problème | Suggestion | Bénéfice | Effort |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|
||||||
|
## 3. Automatisations et scripts
|
||||||
|
| Sujet | Observation | Amélioration proposée | Bénéfice |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## 4. Configuration
|
||||||
|
| Sujet | Observation | Amélioration proposée | Bénéfice |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## 5. Dashboards et ergonomie
|
||||||
|
|
||||||
|
## 6. Priorités recommandées
|
||||||
|
|
||||||
|
## 7. Suggestions écartées faute de documentation officielle compatible
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Copier uniquement si l'utilisateur fournit explicitement ces valeurs.
|
||||||
|
# Conserver ce fichier hors dépôt Git et avec permissions 600.
|
||||||
|
HA_URL=
|
||||||
|
HA_USER=
|
||||||
|
HA_PASSWORD=
|
||||||
|
HA_TOKEN=
|
||||||
|
|
||||||
|
# Accès SSH optionnel
|
||||||
|
HA_SSH_HOST=
|
||||||
|
HA_SSH_PORT=22
|
||||||
|
HA_SSH_USER=
|
||||||
|
HA_SSH_PASSWORD=
|
||||||
|
HA_SSH_KEY_PATH=
|
||||||
|
|
||||||
|
# Accès SSH dédié aux logs HAOS via l'add-on officiel Terminal & SSH
|
||||||
|
HA_LOG_SSH_HOST=
|
||||||
|
HA_LOG_SSH_PORT=
|
||||||
|
HA_LOG_SSH_USER=
|
||||||
|
HA_LOG_SSH_PASSWORD=
|
||||||
|
HA_LOG_SSH_KEY_PATH=
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# memory.md
|
||||||
|
|
||||||
|
## 1. Identité de l'installation
|
||||||
|
- Nom / site :
|
||||||
|
- Version Home Assistant observée :
|
||||||
|
- Type d'installation :
|
||||||
|
- Date de dernière mise à jour de cette mémoire :
|
||||||
|
|
||||||
|
## 2. Architecture et matériel
|
||||||
|
- Hôte :
|
||||||
|
- CPU :
|
||||||
|
- Mémoire :
|
||||||
|
- Stockage :
|
||||||
|
- Particularités réseau :
|
||||||
|
|
||||||
|
## 3. Accès disponibles
|
||||||
|
| Accès | Disponible | Détail non sensible |
|
||||||
|
|---|---|---|
|
||||||
|
| Interface web | | |
|
||||||
|
| API Home Assistant | | |
|
||||||
|
| SSH | | |
|
||||||
|
| MCP | | |
|
||||||
|
|
||||||
|
## 4. Fichiers et emplacements importants
|
||||||
|
| Élément | Chemin / remarque |
|
||||||
|
|---|---|
|
||||||
|
| configuration principale | |
|
||||||
|
| automatisations | |
|
||||||
|
| scripts | |
|
||||||
|
| dashboards | |
|
||||||
|
| logs | |
|
||||||
|
|
||||||
|
## 5. Intégrations importantes
|
||||||
|
| Intégration | Rôle | Remarques |
|
||||||
|
|---|---|---|
|
||||||
|
|
||||||
|
## 6. Conventions locales
|
||||||
|
- Nommage des entités :
|
||||||
|
- Zones / labels :
|
||||||
|
- Helpers importants :
|
||||||
|
- Particularités de dashboards :
|
||||||
|
|
||||||
|
## 7. Problèmes connus et historique utile
|
||||||
|
| Date | Sujet | État | Commentaire |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## 8. Décisions et préférences de maintenance
|
||||||
|
-
|
||||||
|
|
||||||
|
## 9. Points à revoir au prochain audit
|
||||||
|
-
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# repair.md
|
||||||
|
|
||||||
|
## 1. Contexte vérifié
|
||||||
|
- Version Home Assistant :
|
||||||
|
- Type d'installation :
|
||||||
|
- Date de collecte :
|
||||||
|
- Sources analysées :
|
||||||
|
|
||||||
|
## 2. Synthèse matériel
|
||||||
|
| Élément | Total | Utilisé | Libre | Source |
|
||||||
|
|---|---:|---:|---:|---|
|
||||||
|
| CPU | | | | |
|
||||||
|
| Mémoire | | | | |
|
||||||
|
| Swap | | | | |
|
||||||
|
| Disque / volume | | | | |
|
||||||
|
|
||||||
|
## 3. Résumé exécutif
|
||||||
|
|
||||||
|
## 4. Problèmes détectés
|
||||||
|
| Sévérité | Domaine | Constat | Impact | Preuve |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|
||||||
|
## 5. Intégrations fautives
|
||||||
|
|
||||||
|
## 6. Dashboards
|
||||||
|
|
||||||
|
## 7. Entités problématiques
|
||||||
|
|
||||||
|
## 8. Configuration YAML
|
||||||
|
|
||||||
|
## 9. Corrections proposées
|
||||||
|
| Correction | Justification officielle | URL officielle | Compatibilité version | Confiance | Action |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|
||||||
|
## 10. Points non résolus
|
||||||
|
|
||||||
|
## 11. Données complémentaires utiles
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Historique de ha-log-investigator
|
||||||
|
|
||||||
|
## 2026-05-16 — Création initiale
|
||||||
|
- Création du skill `ha-log-investigator`.
|
||||||
|
- Ajout des livrables `repair.md`, `best_entity.md`, `memory.md`.
|
||||||
|
- Ajout des règles de preuve officielle, d'accès minimal et de non-invention.
|
||||||
|
|
||||||
|
## 2026-05-16 — Ajout de l'accès SSH
|
||||||
|
- Ajout des champs SSH dans le modèle d'identifiants.
|
||||||
|
- Ajout d'un collecteur SSH en lecture seule.
|
||||||
|
- Ajout de la préférence pour clé SSH, avec compatibilité mot de passe.
|
||||||
|
|
||||||
|
## 2026-05-16 — Ajout de la mémoire durable
|
||||||
|
- Ajout du livrable `memory.md` pour conserver les éléments pérennes de l'installation.
|
||||||
|
|
||||||
|
## 2026-05-16 — Amélioration après test réel
|
||||||
|
- Correction du collecteur pour Home Assistant OS / BusyBox.
|
||||||
|
- Remplacement de la collecte `scp` par une lecture distante plus robuste.
|
||||||
|
- Ajout de la collecte des fichiers Lovelace, réparations et registres utiles.
|
||||||
|
|
||||||
|
## 2026-05-16 — Gestion moderne des logs HAOS
|
||||||
|
- Évolution constatée : sur Home Assistant OS, `/config/home-assistant.log` n'est plus une source attendue par défaut.
|
||||||
|
- Source officielle : documentation Home Assistant `logger`.
|
||||||
|
- Mise à jour du skill pour privilégier `Settings > System > Logs`, `ha core logs` si autorisé, ou le fichier dupliqué seulement si activé.
|
||||||
|
- Ajout de la détection explicite de l'absence de fichier log sans la traiter comme anomalie.
|
||||||
|
|
||||||
|
## 2026-05-16 — Sécurité et réversibilité
|
||||||
|
- Ajout du compactage optionnel avec `rtk log` côté machine de travail.
|
||||||
|
- Ajout des URLs officielles dans les recommandations.
|
||||||
|
- Ajout d'une trace de retour arrière obligatoire avant suppression demandée.
|
||||||
|
|
||||||
|
## À surveiller
|
||||||
|
- Évolutions futures des APIs ou commandes officielles de collecte des logs.
|
||||||
|
- Changements de structure des registres `.storage`.
|
||||||
|
- Évolutions des mécanismes de réparations Home Assistant.
|
||||||
|
|
||||||
|
## 2026-05-16 — Distinction des add-ons SSH pour les logs HAOS
|
||||||
|
- Évolution constatée pendant le test réel : `ha core logs` fonctionne depuis l'add-on officiel `Terminal & SSH`, alors qu'un autre accès SSH a renvoyé `401 Unauthorized`.
|
||||||
|
- Source officielle : documentation `Common tasks - Operating System` et documentation de l'add-on officiel `Terminal & SSH`.
|
||||||
|
- Mise à jour prévue : préférer explicitement `Terminal & SSH` pour les logs Home Assistant OS et tracer l'add-on utilisé pour chaque collecte.
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-16 — Collecte de logs via session SSH interactive
|
||||||
|
- Évolution constatée pendant le test réel : `ha core logs` renvoie `401 Unauthorized` en commande SSH distante directe, mais fonctionne dans une vraie session interactive avec TTY.
|
||||||
|
- Conséquence : le skill doit tester le mode interactif avant de conclure que les logs Core sont inaccessibles.
|
||||||
|
- Mise à jour : ajout d'une règle spécifique dans l'analyse des logs et la collecte SSH.
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-16 — Priorité au fichier log dupliqué quand présent
|
||||||
|
- Évolution constatée pendant le test réel : après activation du mode de compatibilité, `/config/home-assistant.log` est de nouveau disponible et exploitable.
|
||||||
|
- Mise à jour : le collecteur préfère désormais le fichier dupliqué non vide avant de tenter `ha core logs` ou le mode interactif.
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-16 — Intégration de Watchman dans l'audit
|
||||||
|
- Ajout de Watchman comme source complémentaire obligatoire lorsqu'il est installé.
|
||||||
|
- Ajout de la lecture du rapport final `watchman_report.txt`.
|
||||||
|
- Ajout d'une règle de croisement avec les logs, les dashboards et l'état réel des entités afin de tenir compte des limites heuristiques de Watchman.
|
||||||
|
|
||||||
|
## 2026-05-16 — Intégration de Spook dans l'audit
|
||||||
|
- Ajout de Spook comme complément pour le suivi des réparations et la gestion d'issues visibles dans le dashboard Repairs.
|
||||||
|
- Ajout d'une règle de croisement avec les logs, Watchman et l'état réel des entités.
|
||||||
|
- Spook est utilisé pour suivre et matérialiser les problèmes, pas comme source unique de vérité.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Accès et preuves
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Toujours choisir l'accès le moins intrusif qui permet de répondre correctement.
|
||||||
|
|
||||||
|
## Ordre de préférence
|
||||||
|
|
||||||
|
1. **Artefacts locaux fournis par l'utilisateur**
|
||||||
|
- `home-assistant.log`
|
||||||
|
- `configuration.yaml` et fichiers inclus
|
||||||
|
- exports ou captures de dashboards
|
||||||
|
- diagnostics téléchargés depuis une intégration
|
||||||
|
- sorties de commandes déjà collectées
|
||||||
|
|
||||||
|
2. **API Home Assistant avec token longue durée**
|
||||||
|
- utile pour récupérer version, états, registres, services et diagnostics exposés
|
||||||
|
- préférer le token au mot de passe
|
||||||
|
|
||||||
|
3. **SSH**
|
||||||
|
- utile seulement pour métriques hôte, journaux système, stockage, fichiers non exportés, commandes supervisor/core selon le type d'installation
|
||||||
|
- justifier pourquoi SSH est nécessaire avant de le demander
|
||||||
|
- si SSH est retenu, collecter au minimum : hôte, port, utilisateur, puis soit mot de passe soit chemin de clé privée
|
||||||
|
- préférer l'authentification par clé quand elle est disponible ; utiliser le mot de passe seulement si c'est le mode réellement fourni
|
||||||
|
|
||||||
|
4. **MCP**
|
||||||
|
- optionnel, jamais requis par principe
|
||||||
|
- l'utiliser uniquement s'il existe déjà et apporte un accès structuré, traçable ou plus sûr qu'un accès ad hoc
|
||||||
|
|
||||||
|
## Preuves minimales à réunir
|
||||||
|
|
||||||
|
- version exacte de Home Assistant Core
|
||||||
|
- date/heure de collecte
|
||||||
|
- type d'installation si connu ; si le shell distant est isolé dans un add-on, corroborer avec l'API ou d'autres indices au lieu de conclure trop vite à `unknown`
|
||||||
|
- sources de logs inspectées
|
||||||
|
- liste des fichiers de configuration lus
|
||||||
|
- provenance des métriques matériel
|
||||||
|
- liste des dashboards inspectés
|
||||||
|
- liste des entités réellement observées
|
||||||
|
|
||||||
|
## Commandes ou données typiquement utiles
|
||||||
|
|
||||||
|
Les commandes exactes dépendent du mode d'installation et ne doivent être proposées qu'après vérification documentaire compatible avec la version installée.
|
||||||
|
|
||||||
|
Exemples de catégories de données utiles :
|
||||||
|
- version de Home Assistant
|
||||||
|
- CPU et mémoire
|
||||||
|
- espace disque
|
||||||
|
- extraits de logs autour des erreurs, selon la source officielle adaptée au mode d'installation
|
||||||
|
- registres d'entités et d'appareils
|
||||||
|
- contenu des dashboards YAML ou exports équivalents
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Trace de changements et retour arrière
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Ne jamais supprimer une entité, un service, une automatisation, un script ou un élément de dashboard sans créer d'abord une trace permettant de revenir en arrière.
|
||||||
|
|
||||||
|
## Avant toute suppression demandée par l'utilisateur
|
||||||
|
|
||||||
|
Créer un dossier horodaté `rollback/YYYYMMDDTHHMMSSZ/` contenant, selon le cas :
|
||||||
|
- une copie des fichiers YAML concernés ;
|
||||||
|
- une copie des extraits `.storage` concernés si lisibles ;
|
||||||
|
- un export ou inventaire des entités ciblées ;
|
||||||
|
- un fichier `changes.md` décrivant :
|
||||||
|
- ce qui va être supprimé ;
|
||||||
|
- pourquoi ;
|
||||||
|
- les preuves ;
|
||||||
|
- les fichiers touchés ;
|
||||||
|
- la procédure de restauration.
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Ne jamais mettre de secret dans `changes.md`.
|
||||||
|
- Préférer une désactivation ou un retrait ciblé à une suppression large quand l'intention de l'utilisateur est ambiguë.
|
||||||
|
- Après modification, produire un résumé avant/après.
|
||||||
|
- Si la suppression concerne une entité ou une automatisation utilisée par un dashboard, signaler explicitement les dépendances avant exécution.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Analyse des logs
|
||||||
|
|
||||||
|
## Collecte
|
||||||
|
|
||||||
|
- Conserver la source brute quand elle existe.
|
||||||
|
- Si `rtk` est disponible localement, produire en complément une version compacte avec `rtk log` pour réduire le volume transmis au modèle.
|
||||||
|
- Ne jamais remplacer la source brute par le résumé compact.
|
||||||
|
|
||||||
|
## Stratégie selon le type d'installation
|
||||||
|
|
||||||
|
### Home Assistant OS
|
||||||
|
- Ne pas attendre de fichier `/config/home-assistant.log` par défaut.
|
||||||
|
- Si un fichier dupliqué existe et n'est pas vide, le préférer pour la collecte automatisée car il évite un shell interactif et garde le flux brut disponible.
|
||||||
|
- Considérer comme sources officielles prioritaires :
|
||||||
|
1. l'interface `Settings > System > Logs` ;
|
||||||
|
2. `ha core logs` depuis l'add-on officiel `Terminal & SSH` si l'accès SSH autorisé le permet ;
|
||||||
|
3. `/config/home-assistant.log` seulement si le mode `duplicate-log-file` a été activé volontairement.
|
||||||
|
- Si plusieurs add-ons SSH sont installés, ne pas supposer qu'ils donnent le même accès : tracer l'add-on réellement utilisé pour la collecte.
|
||||||
|
- Si `ha core logs` retourne `401 Unauthorized` en commande distante directe mais fonctionne en session ouverte, tester une session SSH interactive avec TTY (`ssh -tt`) : certains environnements d'add-on initialisent l'accès Supervisor seulement dans ce contexte.
|
||||||
|
|
||||||
|
### Home Assistant Container / Core
|
||||||
|
- Vérifier les fichiers logs de configuration quand ils existent et les commandes adaptées au mode d'installation.
|
||||||
|
|
||||||
|
## Cas où aucune source de log exploitable n'est disponible
|
||||||
|
|
||||||
|
Vérifier séparément :
|
||||||
|
1. le type d'installation ;
|
||||||
|
2. les sources officiellement attendues pour ce type ;
|
||||||
|
3. l'existence éventuelle de fichiers dupliqués ;
|
||||||
|
4. les erreurs d'autorisation éventuelles sur les commandes documentées.
|
||||||
|
|
||||||
|
Si aucune source exploitable n'est accessible :
|
||||||
|
- l'écrire explicitement dans `repair.md` ;
|
||||||
|
- demander un export des logs depuis l'interface Home Assistant ou une source équivalente fournie par l'utilisateur ;
|
||||||
|
- ne pas conclure à l'absence d'erreurs runtime.
|
||||||
|
|
||||||
|
## Usage de `rtk`
|
||||||
|
|
||||||
|
Exemple local :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk log home-assistant.log > home-assistant.compact.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résumé compact sert à l'analyse rapide ; les constats importants doivent rester traçables vers le log brut.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Politique de documentation officielle
|
||||||
|
|
||||||
|
## Règle principale
|
||||||
|
|
||||||
|
Ne recommander une correction que si elle est soutenue par une documentation officielle Home Assistant compatible avec la version réellement installée.
|
||||||
|
|
||||||
|
## Sources acceptables
|
||||||
|
|
||||||
|
Privilégier, selon le sujet :
|
||||||
|
- la documentation officielle Home Assistant ;
|
||||||
|
- les notes de version officielles ;
|
||||||
|
- la documentation développeur officielle Home Assistant ;
|
||||||
|
- les pages officielles d'intégration Home Assistant.
|
||||||
|
|
||||||
|
Les forums, blogs, dépôts personnels et réponses communautaires peuvent servir d'indices, jamais de preuve finale pour une correction.
|
||||||
|
|
||||||
|
## Méthode de validation
|
||||||
|
|
||||||
|
Pour chaque recommandation :
|
||||||
|
1. noter la version installée ;
|
||||||
|
2. vérifier que la fonctionnalité ou le paramètre existe pour cette version ;
|
||||||
|
3. citer la source officielle consultée ;
|
||||||
|
4. indiquer si la recommandation est :
|
||||||
|
- confirmée ;
|
||||||
|
- probable mais à vérifier ;
|
||||||
|
- non démontrée faute de documentation officielle.
|
||||||
|
|
||||||
|
## Si la documentation manque
|
||||||
|
|
||||||
|
Écrire explicitement qu'aucune correction certaine n'est proposée sans documentation officielle compatible. Donner seulement :
|
||||||
|
- les faits observés ;
|
||||||
|
- les vérifications complémentaires possibles ;
|
||||||
|
- les informations à collecter pour lever l'incertitude.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Mise à jour du skill
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Mettre à jour `ha-log-investigator` lorsqu'une évolution officielle de Home Assistant modifie la manière correcte de collecter les preuves, d'interroger l'API, de lire les logs, de traiter les dashboards, les réparations ou les fichiers de configuration.
|
||||||
|
|
||||||
|
## Déclencheurs
|
||||||
|
|
||||||
|
Lancer ce workflow si :
|
||||||
|
- une documentation officielle contredit le comportement actuel du skill ;
|
||||||
|
- une commande documentée change ;
|
||||||
|
- une API officielle évolue ;
|
||||||
|
- un mode d'installation modifie ses chemins ou ses sources de logs ;
|
||||||
|
- un test réel révèle une hypothèse devenue obsolète.
|
||||||
|
|
||||||
|
## Workflow de mise à jour
|
||||||
|
|
||||||
|
1. Identifier l'évolution constatée.
|
||||||
|
2. Vérifier l'information dans une source officielle Home Assistant.
|
||||||
|
3. Décrire l'écart entre :
|
||||||
|
- comportement actuel du skill ;
|
||||||
|
- comportement officiel attendu.
|
||||||
|
4. Mettre à jour uniquement les fichiers nécessaires :
|
||||||
|
- `SKILL.md`
|
||||||
|
- références concernées
|
||||||
|
- scripts concernés
|
||||||
|
- templates concernés
|
||||||
|
5. Ajouter une entrée dans `history.md` avec :
|
||||||
|
- date ;
|
||||||
|
- évolution observée ;
|
||||||
|
- source officielle ;
|
||||||
|
- fichiers modifiés ;
|
||||||
|
- conséquence pratique.
|
||||||
|
6. Revalider le skill.
|
||||||
|
7. Si possible, retester sur un cas réel ou un artefact réaliste.
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Ne jamais supprimer l'historique existant.
|
||||||
|
- Ne pas mettre à jour le skill sur la base d'un forum, d'un blog ou d'une intuition seule.
|
||||||
|
- Si une évolution est suspectée mais non confirmée officiellement, l'ajouter dans la section `À surveiller` de `history.md` au lieu de modifier le comportement du skill.
|
||||||
|
- Conserver la compatibilité avec les versions anciennes seulement si elle est encore utile et documentée.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Spook
|
||||||
|
|
||||||
|
## Rôle
|
||||||
|
|
||||||
|
Utiliser Spook lorsqu'il est installé comme complément pour la gestion des réparations Home Assistant.
|
||||||
|
|
||||||
|
Spook fournit des actions et entités autour du dashboard Repairs :
|
||||||
|
- créer des issues ;
|
||||||
|
- ignorer / réactiver des issues ;
|
||||||
|
- compter les issues actives, ignorées et totales ;
|
||||||
|
- suivre des problèmes récurrents dans un format visible dans Home Assistant.
|
||||||
|
|
||||||
|
## Règles d'usage
|
||||||
|
|
||||||
|
- Vérifier si Spook est installé avant de s'y fier.
|
||||||
|
- L'utiliser comme outil de suivi et d'annotation des problèmes, pas comme source unique de vérité.
|
||||||
|
- Croiser les issues Spook avec :
|
||||||
|
- les logs runtime ;
|
||||||
|
- l'état réel des entités ;
|
||||||
|
- Watchman ;
|
||||||
|
- la configuration YAML ;
|
||||||
|
- les réparations natives Home Assistant.
|
||||||
|
|
||||||
|
## Quand l'utiliser
|
||||||
|
|
||||||
|
- Quand tu veux matérialiser un problème récurrent dans Repairs.
|
||||||
|
- Quand tu veux suivre l'état "à traiter / traité" pendant un nettoyage progressif.
|
||||||
|
- Quand tu veux signaler une anomalie de façon visible dans Home Assistant sans modifier la logique métier.
|
||||||
|
|
||||||
|
## Ce qu'il faut retenir
|
||||||
|
|
||||||
|
- Spook est utile pour le pilotage du flux de corrections.
|
||||||
|
- Spook ne remplace pas Watchman.
|
||||||
|
- Spook ne remplace pas l'analyse des logs.
|
||||||
|
- Spook ne remplace pas la correction de la cause racine.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Collecteur SSH
|
||||||
|
|
||||||
|
Utiliser `scripts/collect_ssh_evidence.sh` lorsque SSH est justifié et que l'utilisateur veut une collecte reproductible en lecture seule.
|
||||||
|
|
||||||
|
## Entrées attendues
|
||||||
|
|
||||||
|
Un fichier d'identifiants au format `credentials.env` contenant au minimum :
|
||||||
|
- `HA_SSH_HOST`
|
||||||
|
- `HA_SSH_PORT`
|
||||||
|
- `HA_SSH_USER`
|
||||||
|
- soit `HA_SSH_PASSWORD`, soit `HA_SSH_KEY_PATH`
|
||||||
|
|
||||||
|
Préférer `HA_SSH_KEY_PATH` si disponible.
|
||||||
|
|
||||||
|
## Exemple
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/collect_ssh_evidence.sh \
|
||||||
|
--credentials ~/.ha-log-investigator/credentials.env \
|
||||||
|
--output ./ha-evidence
|
||||||
|
```
|
||||||
|
|
||||||
|
## Garanties
|
||||||
|
|
||||||
|
- collecte en lecture seule ;
|
||||||
|
- aucune modification distante ;
|
||||||
|
- sorties regroupées dans un dossier horodaté ;
|
||||||
|
- absence de secrets dans les rapports générés par le script.
|
||||||
|
|
||||||
|
## Données collectées
|
||||||
|
|
||||||
|
- système : `uname`, OS, CPU, mémoire, swap, disques ;
|
||||||
|
- indices de version Home Assistant accessibles localement ;
|
||||||
|
- extrait de logs Home Assistant ;
|
||||||
|
- fichiers YAML principaux si accessibles ;
|
||||||
|
- index des fichiers `.storage` si accessible.
|
||||||
|
|
||||||
|
## Limites
|
||||||
|
|
||||||
|
- les chemins varient selon le mode d'installation ;
|
||||||
|
- l'authentification par mot de passe exige `sshpass` côté machine cliente ;
|
||||||
|
- le script collecte des preuves, il ne remplace pas la validation par documentation officielle avant recommandation.
|
||||||
|
|
||||||
|
|
||||||
|
## Plusieurs add-ons SSH
|
||||||
|
|
||||||
|
Sur Home Assistant OS, préférer l'add-on officiel `Terminal & SSH` lorsqu'il faut collecter `ha core logs`.
|
||||||
|
Si un autre add-on SSH est utilisé pour d'autres tâches, conserver des profils d'accès distincts et consigner lequel a servi à chaque collecte.
|
||||||
|
|
||||||
|
|
||||||
|
## Cas `ha core logs` interactif uniquement
|
||||||
|
|
||||||
|
Si `ha core logs` fonctionne après connexion interactive mais renvoie `401 Unauthorized` lorsqu'il est exécuté directement via `ssh host 'ha core logs'`, utiliser un mode interactif avec TTY pour la collecte des logs, puis nettoyer les séquences ANSI et le banner avant analyse.
|
||||||
|
|
||||||
|
Cette situation doit être notée dans `repair.md` comme une contrainte d'accès, pas comme une erreur Home Assistant.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Watchman
|
||||||
|
|
||||||
|
## Rôle
|
||||||
|
|
||||||
|
Utiliser Watchman lorsqu'il est installé pour compléter l'audit Home Assistant.
|
||||||
|
|
||||||
|
Watchman scanne les fichiers de configuration et signale les entités et actions/services référencés mais manquants ou indisponibles. Il produit un rapport texte, généralement `watchman_report.txt`, et peut aussi retourner un rapport via l'action `watchman.report`.
|
||||||
|
|
||||||
|
## Règles d'usage
|
||||||
|
|
||||||
|
- Vérifier si Watchman est installé avant l'analyse finale.
|
||||||
|
- S'il est installé, lire le rapport le plus récent et l'intégrer aux constats.
|
||||||
|
- Ne pas traiter automatiquement chaque ligne Watchman comme une vérité absolue : Watchman utilise une analyse heuristique et peut produire des faux positifs ou faux négatifs.
|
||||||
|
- Croiser ses résultats avec :
|
||||||
|
- l'état réel des entités ;
|
||||||
|
- les dashboards ;
|
||||||
|
- les automatisations ;
|
||||||
|
- les logs runtime.
|
||||||
|
|
||||||
|
## Rapport attendu
|
||||||
|
|
||||||
|
Chercher en priorité :
|
||||||
|
- `/config/watchman_report.txt`
|
||||||
|
- ou le chemin configuré dans Watchman si différent.
|
||||||
|
|
||||||
|
## Quand produire le rapport
|
||||||
|
|
||||||
|
Si nécessaire et si l'utilisateur autorise l'action, exécuter `watchman.report` depuis Home Assistant avec création de fichier afin d'obtenir un rapport actualisé avant l'analyse finale.
|
||||||
|
|
||||||
|
## Ce qu'il faut reprendre dans les livrables
|
||||||
|
|
||||||
|
Dans `repair.md` :
|
||||||
|
- nombre d'entités manquantes ;
|
||||||
|
- nombre d'actions/services manquants ;
|
||||||
|
- exemples significatifs ;
|
||||||
|
- indication que le rapport vient de Watchman.
|
||||||
|
|
||||||
|
Dans `best_entity.md` :
|
||||||
|
- pistes de nettoyage des références orphelines ;
|
||||||
|
- priorités d'amélioration si elles sont confirmées par d'autres sources.
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
collect_ssh_evidence.sh --credentials /path/to/credentials.env [--output /path/to/output]
|
||||||
|
|
||||||
|
Collecte en lecture seule via SSH :
|
||||||
|
- informations système (CPU, mémoire, swap, disques)
|
||||||
|
- indices sur la version Home Assistant
|
||||||
|
- extraits de logs si accessibles
|
||||||
|
- fichiers de configuration principaux si accessibles
|
||||||
|
|
||||||
|
Le script n'effectue aucune modification distante.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials_file=""
|
||||||
|
output_dir=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--credentials)
|
||||||
|
credentials_file="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
output_dir="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument inconnu: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$credentials_file" ]]; then
|
||||||
|
echo "--credentials est obligatoire" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$credentials_file" ]]; then
|
||||||
|
echo "Fichier d'identifiants introuvable: $credentials_file" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$credentials_file"
|
||||||
|
|
||||||
|
: "${HA_SSH_HOST:?HA_SSH_HOST manquant}"
|
||||||
|
: "${HA_SSH_USER:?HA_SSH_USER manquant}"
|
||||||
|
HA_SSH_PORT="${HA_SSH_PORT:-22}"
|
||||||
|
HA_SSH_PASSWORD="${HA_SSH_PASSWORD:-}"
|
||||||
|
HA_SSH_KEY_PATH="${HA_SSH_KEY_PATH:-}"
|
||||||
|
|
||||||
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
if [[ -z "$output_dir" ]]; then
|
||||||
|
output_dir="./ha-ssh-evidence-$timestamp"
|
||||||
|
fi
|
||||||
|
mkdir -p "$output_dir"/{system,home-assistant,config}
|
||||||
|
chmod 700 "$output_dir"
|
||||||
|
|
||||||
|
ssh_base=(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT")
|
||||||
|
scp_base=(scp -P "$HA_SSH_PORT" -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||||
|
|
||||||
|
if [[ -n "$HA_SSH_KEY_PATH" ]]; then
|
||||||
|
ssh_base+=(-i "$HA_SSH_KEY_PATH")
|
||||||
|
scp_base+=(-i "$HA_SSH_KEY_PATH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_ssh() {
|
||||||
|
local remote_cmd="$1"
|
||||||
|
if [[ -n "$HA_SSH_PASSWORD" && -z "$HA_SSH_KEY_PATH" ]]; then
|
||||||
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
|
echo "sshpass est requis pour l'authentification par mot de passe, mais il est absent." >&2
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
sshpass -p "$HA_SSH_PASSWORD" ssh -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT" "$HA_SSH_USER@$HA_SSH_HOST" "$remote_cmd"
|
||||||
|
else
|
||||||
|
"${ssh_base[@]}" "$HA_SSH_USER@$HA_SSH_HOST" "$remote_cmd"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_remote_file() {
|
||||||
|
local remote_path="$1"
|
||||||
|
local local_path="$2"
|
||||||
|
run_ssh "if [ -r '$remote_path' ]; then cat '$remote_path'; fi" > "$local_path" 2>/dev/null || true
|
||||||
|
if [[ ! -s "$local_path" ]]; then
|
||||||
|
rm -f "$local_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_interactive_core_logs() {
|
||||||
|
local lines="$1"
|
||||||
|
local raw_path="$2"
|
||||||
|
local clean_path="$3"
|
||||||
|
local start_marker="__HA_LOG_START__"
|
||||||
|
local end_marker="__HA_LOG_END__"
|
||||||
|
|
||||||
|
if [[ -n "$HA_SSH_PASSWORD" && -z "$HA_SSH_KEY_PATH" ]]; then
|
||||||
|
if ! command -v sshpass >/dev/null 2>&1; then
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
printf 'echo %s\nha core logs --lines %s\necho %s\nexit\n' "$start_marker" "$lines" "$end_marker" \
|
||||||
|
| sshpass -p "$HA_SSH_PASSWORD" ssh -tt -o StrictHostKeyChecking=accept-new -p "$HA_SSH_PORT" "$HA_SSH_USER@$HA_SSH_HOST" \
|
||||||
|
> "$raw_path" 2>&1 || true
|
||||||
|
else
|
||||||
|
printf 'echo %s\nha core logs --lines %s\necho %s\nexit\n' "$start_marker" "$lines" "$end_marker" \
|
||||||
|
| "${ssh_base[@]}" -tt "$HA_SSH_USER@$HA_SSH_HOST" \
|
||||||
|
> "$raw_path" 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$raw_path" "$clean_path" "$start_marker" "$end_marker" <<'PY2'
|
||||||
|
import re, sys
|
||||||
|
from pathlib import Path
|
||||||
|
raw_path, clean_path, start_marker, end_marker = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3], sys.argv[4]
|
||||||
|
text = raw_path.read_text(errors='ignore')
|
||||||
|
text = re.sub(r'\x1B\][^\x07]*(?:\x07|\x1b\\)', '', text)
|
||||||
|
text = re.sub(r'\x1B\[[0-9;?]*[ -/]*[@-~]', '', text)
|
||||||
|
text = text.replace('\r', '')
|
||||||
|
lines = text.splitlines()
|
||||||
|
# The command script itself is echoed before execution. Keep the *last* start marker,
|
||||||
|
# which is the marker printed by the shell, then the last matching end marker after it.
|
||||||
|
start_idx = max((i for i,l in enumerate(lines) if start_marker in l), default=-1)
|
||||||
|
end_idx = max((i for i,l in enumerate(lines) if end_marker in l and i > start_idx), default=-1)
|
||||||
|
if start_idx != -1 and end_idx != -1:
|
||||||
|
body = lines[start_idx + 1:end_idx]
|
||||||
|
clean = []
|
||||||
|
for line in body:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
clean.append('')
|
||||||
|
continue
|
||||||
|
if 'ha core logs --lines' in stripped:
|
||||||
|
continue
|
||||||
|
if stripped == start_marker or stripped == end_marker or stripped == 'exit':
|
||||||
|
continue
|
||||||
|
if stripped.startswith('➜') or stripped == '#':
|
||||||
|
continue
|
||||||
|
clean.append(line)
|
||||||
|
clean_path.write_text('\n'.join(clean).strip() + '\n')
|
||||||
|
PY2
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "collected_at_utc=$timestamp"
|
||||||
|
echo "ssh_host=$HA_SSH_HOST"
|
||||||
|
echo "ssh_port=$HA_SSH_PORT"
|
||||||
|
echo "ssh_user=$HA_SSH_USER"
|
||||||
|
echo "install_type_hint_source=ssh_shell_only"
|
||||||
|
} > "$output_dir/collection.meta"
|
||||||
|
|
||||||
|
run_ssh 'uname -a || true' > "$output_dir/system/uname.txt" || true
|
||||||
|
run_ssh 'cat /etc/os-release 2>/dev/null || true' > "$output_dir/system/os-release.txt" || true
|
||||||
|
run_ssh 'if [ -d /usr/share/hassio ] || [ -e /data/supervisor ]; then echo home_assistant_os_or_supervised; else echo unknown; fi' > "$output_dir/home-assistant/install-type-hint.txt" || true
|
||||||
|
run_ssh 'nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || true' > "$output_dir/system/cpu-count.txt" || true
|
||||||
|
run_ssh 'lscpu 2>/dev/null || cat /proc/cpuinfo 2>/dev/null || true' > "$output_dir/system/cpu.txt" || true
|
||||||
|
run_ssh 'free -h 2>/dev/null || cat /proc/meminfo 2>/dev/null || true' > "$output_dir/system/memory.txt" || true
|
||||||
|
run_ssh 'df -hT 2>/dev/null || df -h 2>/dev/null || true' > "$output_dir/system/disks.txt" || true
|
||||||
|
run_ssh 'swapon --show 2>/dev/null || true' > "$output_dir/system/swap.txt" || true
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
if command -v ha >/dev/null 2>&1; then
|
||||||
|
ha core info 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
' > "$output_dir/home-assistant/ha-core-info.txt" || true
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
' > "$output_dir/home-assistant/docker-ps.txt" || true
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
for f in \
|
||||||
|
/config/.HA_VERSION \
|
||||||
|
/usr/share/hassio/homeassistant/.HA_VERSION \
|
||||||
|
/home/homeassistant/.homeassistant/.HA_VERSION; do
|
||||||
|
if [ -r "$f" ]; then
|
||||||
|
echo "=== $f ==="
|
||||||
|
cat "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
' > "$output_dir/home-assistant/version-files.txt" || true
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
for f in \
|
||||||
|
/config/home-assistant.log \
|
||||||
|
/usr/share/hassio/homeassistant/home-assistant.log \
|
||||||
|
/home/homeassistant/.homeassistant/home-assistant.log; do
|
||||||
|
if [ -r "$f" ] && [ -s "$f" ]; then
|
||||||
|
echo "=== $f ==="
|
||||||
|
tail -n 400 "$f"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
' > "$output_dir/home-assistant/home-assistant-log-tail.txt" || true
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
echo "current_log_exists=$( [ -r /config/home-assistant.log ] && echo yes || echo no )"
|
||||||
|
echo "haos_default_log_file_expected=no"
|
||||||
|
for f in /config/home-assistant.log /config/home-assistant.log.1 /config/home-assistant.log.fault; do
|
||||||
|
if [ -e "$f" ]; then
|
||||||
|
size=$(wc -c < "$f" 2>/dev/null || echo unknown)
|
||||||
|
echo "$f bytes=$size"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
' > "$output_dir/home-assistant/log-status.txt" || true
|
||||||
|
|
||||||
|
run_ssh 'ha core logs --lines 40 2>&1 || true' > "$output_dir/home-assistant/ha-core-logs-attempt.txt" || true
|
||||||
|
|
||||||
|
if grep -q '401: Unauthorized' "$output_dir/home-assistant/ha-core-logs-attempt.txt" 2>/dev/null; then
|
||||||
|
collect_interactive_core_logs 120 "$output_dir/home-assistant/ha-core-logs-interactive.raw.txt" "$output_dir/home-assistant/ha-core-logs-interactive.txt" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$output_dir/home-assistant/ha-core-logs-interactive.txt" ]] && command -v rtk >/dev/null 2>&1; then
|
||||||
|
rtk log "$output_dir/home-assistant/ha-core-logs-interactive.txt" > "$output_dir/home-assistant/ha-core-logs-interactive.compact.txt" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$output_dir/home-assistant/home-assistant-log-tail.txt" ]] && command -v rtk >/dev/null 2>&1; then
|
||||||
|
rtk log "$output_dir/home-assistant/home-assistant-log-tail.txt" > "$output_dir/home-assistant/home-assistant-log-compact.txt" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
if command -v journalctl >/dev/null 2>&1; then
|
||||||
|
journalctl -u home-assistant@homeassistant.service -n 300 --no-pager 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
' > "$output_dir/home-assistant/journal-home-assistant.txt" || true
|
||||||
|
|
||||||
|
for remote in \
|
||||||
|
/config/configuration.yaml \
|
||||||
|
/config/automations.yaml \
|
||||||
|
/config/scripts.yaml \
|
||||||
|
/config/scenes.yaml \
|
||||||
|
/usr/share/hassio/homeassistant/configuration.yaml \
|
||||||
|
/home/homeassistant/.homeassistant/configuration.yaml; do
|
||||||
|
name="$(echo "$remote" | sed 's#^/##; s#/#__#g')"
|
||||||
|
copy_remote_file "$remote" "$output_dir/config/$name" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
run_ssh '
|
||||||
|
for d in /config/.storage /usr/share/hassio/homeassistant/.storage /home/homeassistant/.homeassistant/.storage; do
|
||||||
|
if [ -d "$d" ]; then
|
||||||
|
echo "=== $d ==="
|
||||||
|
for f in "$d"/*; do
|
||||||
|
[ -f "$f" ] && basename "$f"
|
||||||
|
done | sort
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
' > "$output_dir/config/storage-index.txt" || true
|
||||||
|
|
||||||
|
for remote in \
|
||||||
|
/config/.storage/lovelace.lovelace \
|
||||||
|
/config/.storage/lovelace_dashboards \
|
||||||
|
/config/.storage/lovelace_resources \
|
||||||
|
/config/.storage/repairs.issue_registry \
|
||||||
|
/config/.storage/core.entity_registry \
|
||||||
|
/config/.storage/core.config_entries; do
|
||||||
|
name="$(echo "$remote" | sed 's#^/##; s#/#__#g')"
|
||||||
|
copy_remote_file "$remote" "$output_dir/config/$name" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Collecte terminée: $output_dir"
|
||||||
@@ -161,6 +161,111 @@ assert_eq "cycle update→local" "local" "$(state_get "dev_debugging_claude_code
|
|||||||
rm -f "$STATE_FILE"
|
rm -f "$STATE_FILE"
|
||||||
unset STATE_FILE || true
|
unset STATE_FILE || true
|
||||||
|
|
||||||
|
# ── 5. Détection bundle / legacy ─────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "5. get_frontmatter_agents() / scan_skills() double format"
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
# Skill bundle : SKILL.md + agents: [claude-code, codex]
|
||||||
|
mkdir -p "$TMP_DIR/skills/infra/mon-bundle"
|
||||||
|
cat > "$TMP_DIR/skills/infra/mon-bundle/SKILL.md" << 'EOF'
|
||||||
|
---
|
||||||
|
name: mon-bundle
|
||||||
|
version: 1.2.0
|
||||||
|
agents: [claude-code, codex]
|
||||||
|
description: Skill bundle test
|
||||||
|
tags: [test]
|
||||||
|
---
|
||||||
|
# Mon bundle
|
||||||
|
EOF
|
||||||
|
mkdir -p "$TMP_DIR/skills/infra/mon-bundle/scripts"
|
||||||
|
touch "$TMP_DIR/skills/infra/mon-bundle/scripts/helper.sh"
|
||||||
|
|
||||||
|
# Skill legacy : claude-code.md
|
||||||
|
mkdir -p "$TMP_DIR/skills/dev/ancien"
|
||||||
|
cat > "$TMP_DIR/skills/dev/ancien/claude-code.md" << 'EOF'
|
||||||
|
---
|
||||||
|
name: ancien
|
||||||
|
version: 0.5.0
|
||||||
|
description: Skill legacy
|
||||||
|
tags: [legacy]
|
||||||
|
---
|
||||||
|
# Ancien skill
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Fichier Markdown de support (ne doit PAS être détecté comme skill)
|
||||||
|
mkdir -p "$TMP_DIR/skills/infra/mon-bundle/references"
|
||||||
|
echo "# Ref doc" > "$TMP_DIR/skills/infra/mon-bundle/references/notes.md"
|
||||||
|
|
||||||
|
assert_eq "agents: [claude-code, codex] → 2 agents" \
|
||||||
|
"claude-code
|
||||||
|
codex" \
|
||||||
|
"$(get_frontmatter_agents "$TMP_DIR/skills/infra/mon-bundle/SKILL.md")"
|
||||||
|
|
||||||
|
assert_eq "agents: absent → vide" \
|
||||||
|
"" \
|
||||||
|
"$(get_frontmatter_agents "$TMP_DIR/skills/dev/ancien/claude-code.md")"
|
||||||
|
|
||||||
|
# scan_skills avec les deux formats
|
||||||
|
REPO_DIR="$TMP_DIR"
|
||||||
|
DETECTED_AGENTS=()
|
||||||
|
SKILLS_TAG=""
|
||||||
|
STATE_FILE=$(mktemp)
|
||||||
|
scan_skills 2>/dev/null
|
||||||
|
|
||||||
|
# Compter les entrées trouvées
|
||||||
|
bundle_count=0; legacy_count=0; ref_count=0
|
||||||
|
for e in "${SKILLS_LIST[@]}"; do
|
||||||
|
IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e"
|
||||||
|
[[ "$kind" == "bundle" ]] && (( bundle_count++ )) || true
|
||||||
|
[[ "$kind" == "legacy" ]] && (( legacy_count++ )) || true
|
||||||
|
# Vérifier qu'aucune entrée ne vient de references/notes.md
|
||||||
|
[[ "$source_path" == *"notes.md"* ]] && (( ref_count++ )) || true
|
||||||
|
done
|
||||||
|
|
||||||
|
assert_eq "bundle détecté 2 fois (2 agents)" "2" "$bundle_count"
|
||||||
|
assert_eq "legacy détecté 1 fois" "1" "$legacy_count"
|
||||||
|
assert_eq "fichier references/ ignoré" "0" "$ref_count"
|
||||||
|
|
||||||
|
# Vérifier que kind=bundle et source_path pointent sur le dossier
|
||||||
|
first_bundle=""
|
||||||
|
for e in "${SKILLS_LIST[@]}"; do
|
||||||
|
IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e"
|
||||||
|
[[ "$kind" == "bundle" ]] && { first_bundle="$source_path"; break; }
|
||||||
|
done
|
||||||
|
assert_eq "source_path bundle = dossier du skill" \
|
||||||
|
"$TMP_DIR/skills/infra/mon-bundle" \
|
||||||
|
"$first_bundle"
|
||||||
|
|
||||||
|
# Vérifier que SKILL.md du bundle est lisible pour le preview
|
||||||
|
assert_true "SKILL.md bundle accessible" "[[ -f '${first_bundle}/SKILL.md' ]]"
|
||||||
|
assert_true "scripts/ du bundle accessible" "[[ -f '${first_bundle}/scripts/helper.sh' ]]"
|
||||||
|
|
||||||
|
# Priorité bundle sur legacy : si SKILL.md existe, le <agent>.md ne duplique pas
|
||||||
|
mkdir -p "$TMP_DIR/skills/dev/ancien"
|
||||||
|
cat > "$TMP_DIR/skills/dev/ancien/SKILL.md" << 'EOF'
|
||||||
|
---
|
||||||
|
name: ancien
|
||||||
|
version: 1.0.0
|
||||||
|
agents: [claude-code]
|
||||||
|
description: Skill migré en bundle
|
||||||
|
tags: [test]
|
||||||
|
---
|
||||||
|
# Ancien migré
|
||||||
|
EOF
|
||||||
|
SKILLS_LIST=()
|
||||||
|
scan_skills 2>/dev/null
|
||||||
|
count_ancien=0
|
||||||
|
for e in "${SKILLS_LIST[@]}"; do
|
||||||
|
[[ "$e" == *"|ancien|claude-code|"* ]] && (( count_ancien++ )) || true
|
||||||
|
done
|
||||||
|
assert_eq "pas de doublon bundle+legacy pour même agent" "1" "$count_ancien"
|
||||||
|
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
unset REPO_DIR STATE_FILE SKILLS_TAG DETECTED_AGENTS
|
||||||
|
|
||||||
# ── Bilan ─────────────────────────────────────────────────────────
|
# ── Bilan ─────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "══════════════════════════════════"
|
echo "══════════════════════════════════"
|
||||||
|
|||||||
@@ -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