Compare commits

...

27 Commits

Author SHA1 Message Date
gilles 4f1f11d429 ha skill 2026-05-16 10:43:42 +02:00
gilles 648184da43 fix: install.sh — tags corrompus et section GLOBAL vide
Bug 1 : format_skill_line et print_summary ne lisaient que 8 champs avec
IFS='|' read, donc tags héritait de "tags|bundle|/tmp/..." depuis que
SKILLS_LIST a 10 champs. Ajout de kind et source_path dans tous les read.

Bug 2 : gen_global_section itère sur DETECTED_AGENTS, mais si l'utilisateur
appuie ENTER sans sélectionner dans le menu agents, le tableau est vide →
aucun skill global affiché. Fallback vers les 4 agents connus si vide,
cohérent avec le comportement de _skill_agent_ok().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:40:40 +02:00
gilles 358b0f5289 fix: install.sh — section GLOBAL : détection skills sans sous-dossier catégorie
Problème : .codex/skills/ha-log-investigator/SKILL.md n'a pas de niveau
catégorie contrairement à .claude/skills/infra/ha-log-investigator/SKILL.md.
Le parsing cat/skill était donc faux pour codex (cat=skill, skill="SKILL").

Fix :
- gen_global_section() détecte la profondeur via le nombre de / dans le chemin
- cat_name vide si skill directement sous skills/ (structure codex)
- find exclut les dossiers cachés (*.system*, etc.) via -not -path '*/.*'
- preview_script g: construit le chemin selon présence ou non de cat_name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:31:13 +02:00
gilles 75d833d123 fix: install.sh — détection codex (et autres) via nvm
La détection via command -v échoue dans curl|bash car nvm n'est pas
chargé. Ajout de _find_bin() qui cherche aussi dans :
- ~/.npm-global/bin/
- ~/.local/bin/
- ~/.nvm/versions/node/*/bin/

Corrige la non-détection de codex installé via nvm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:26:30 +02:00
gilles bdf635e547 feat: install.sh — support double format bundle/legacy (amelioration.md)
Double détection dans scan_skills() :
- Bundle : dossier avec SKILL.md, agents: lu dans le frontmatter,
  copie récursive du dossier complet (scripts/, templates/, references/)
- Legacy : <agent>.md existant, agent déduit du nom de fichier
- Priorité bundle sur legacy pour un même cat/skill/agent
- Nouveau champ get_frontmatter_agents() pour parser agents: [...]
- SKILLS_LIST étendu : ...|kind|source_path (10 champs)
- install_selected() branche sur kind=bundle vs legacy
- preview_script utilise source_path pour trouver le fichier à afficher

ha-log-investigator (bundle avec scripts/ et references/) est maintenant
détecté et installé correctement.

Tests : section 5 ajoutée — 9 nouveaux cas (bundle, legacy, références
ignorées, doublon, priorité, source_path, accessibilité fichiers).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:46:12 +02:00
gilles 2fe62335c4 bug skill.md 2026-05-16 06:59:49 +02:00
gilles 3fce51d7f5 ha update 2026-05-16 06:45:26 +02:00
gilles 22f79d68f0 add ha skill 2026-05-16 06:38:22 +02:00
gilles d131eeec5d feat: install.sh — deux sections DÉPÔT/GLOBAL avec bascule TAB
- TAB bascule entre section DÉPÔT (skills repo) et GLOBAL (installés)
- Section active affichée en premier avec header violet ╔══╗
- Section inactive grisée avec catégories repliées en-dessous
- x remplace TAB pour plier/déplier les catégories
- Skills globaux en type g: (navigables + preview)
- ESC dans le menu agents quitte proprement le script
- F1 affiche l'aide dans le panneau preview à droite (v pour revenir)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 06:22:19 +02:00
gilles bd1ddd8193 chore: future-list — ajout skill spécialiste météo France
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 06:08:05 +02:00
gilles a2b592e885 fix: install.sh — couleurs GRV_* avec $'\033' au lieu de '\033'
'\033' = chaîne littérale, interprétée uniquement par echo -e.
$'\033' = vrai octet ESC, fonctionne dans les tableaux bash et printf '%s'.
Corrige l'affichage des codes ANSI bruts dans le menu select_agents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 06:06:29 +02:00
gilles 548a4627f9 fix: install.sh — select_agents : supprimer </dev/tty du fzf
</dev/tty écrasait le pipe printf|fzf, fzf lisait le terminal comme
source de données au lieu des 4 lignes d'agents → 178329 items.
fzf gère /dev/tty en interne pour son UI interactive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 06:04:09 +02:00
gilles f17a4e976a feat: install.sh — touche c pour copier la liste dans le presse-papier
Ajoute un copy_script qui génère la liste visible (champs cachés et codes
ANSI supprimés) et la copie via wl-copy (Wayland), xclip ou xsel en fallback.
Légende et aide F1 mises à jour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 06:02:27 +02:00
gilles 893957703a feat: install.sh — aide F1 avec icônes, raccourcis et variables d'env
Ajoute un fichier d'aide accessible via F1 (less -R) depuis le menu fzf.
Documente : états (✓↑+·), actions (●L●G○), raccourcis (SPACE/TAB/v/F1),
arbre des catégories et variables d'environnement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:59:37 +02:00
gilles 4dde0a9d8b fix: select_agents — printf '%s\n' évite l'interprétation des codes ANSI comme format 2026-05-16 05:51:57 +02:00
gilles 25d0cfb0cb feat: install.sh — sélection agents, footer global, fix focus SPACE
- Menu fzf de sélection d'agents au démarrage (TAB multi-select)
- Footer en bas du tableau : skills déjà installés globalement avec preview
- Fix bug SPACE : +pos({n}) restaure le focus sur l'item modifié

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:49:13 +02:00
gilles 5088ec0189 feat: install.sh — arbre catégories, SPACE=action, TAB=plier, v=viewer bat
- Bug fix: {1} au lieu de {n} — action s'applique bien au skill sélectionné
- SPACE cycle l'action (local→global→ignorer), TAB plie/déplie la catégorie
- Arbre par catégorie : en-têtes ▼/▶, repliage auto si >3 skills
- Touche v : viewer avec coloration syntaxique (bat/batcat ou cat en fallback)
- Preview caché par défaut (v pour afficher), 50% droite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:42:55 +02:00
gilles 436578968e feat: install.sh — description, tags, preview et intro dans le menu fzf
- Ligne : description tronquée + tags #tag visibles
- Preview (45% droite) : contenu complet du skill au survol
- Intro : explication des touches avant le menu
- REPO_DIR exporté dans fns_file pour le script preview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:35:49 +02:00
gilles f40ddcb889 feat: ajout skills test — git-expert et docker-compose (claude-code) 2026-05-16 05:31:10 +02:00
gilles 22b6b9b596 fix: install.sh — supprimer --header-lines=2 (causait compteur 0/0 dans fzf)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:28:45 +02:00
gilles 7b9fb3a231 fix: install.sh — supprimer </dev/tty sur fzf (cassait l'entrée liste)
</dev/tty en dernière position écrasait < <(bash "$list_script").
fzf ouvre /dev/tty lui-même pour les touches clavier, pas besoin de le forcer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:25:51 +02:00
gilles 00e7057708 fix: install.sh — nom fichier fzf sans préfixe v (tag v0.72.0 → fzf-0.72.0-linux_amd64.tar.gz)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:19:23 +02:00
gilles ef0b16879f fix: install.sh — URL fzf correcte via GitHub API (nom inclut la version)
Le fichier s'appelle fzf-{version}-linux_amd64.tar.gz, pas fzf-linux_amd64.tar.gz.
On récupère d'abord le tag via l'API GitHub releases/latest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 05:17:21 +02:00
gilles 4272da744c 2 2026-05-16 05:15:31 +02:00
gilles 64fedcada3 1 2026-05-16 05:14:17 +02:00
gilles e3b9a7f59a fix: install.sh — read et fzf lisent depuis /dev/tty (compatibilité curl | bash)
Sans </dev/tty, stdin est le pipe curl et les prompts interactifs
(question fzf, question installation fzf) ne répondent pas.

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