#!/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 ────────────────────────────────────── GRV_FG='\033[38;5;223m' GRV_RED='\033[38;5;167m' GRV_GREEN='\033[38;5;142m' GRV_YELLOW='\033[38;5;214m' GRV_BLUE='\033[38;5;109m' GRV_PURPLE='\033[38;5;175m' GRV_AQUA='\033[38;5;108m' GRV_ORANGE='\033[38;5;208m' GRV_GRAY='\033[38;5;245m' RESET='\033[0m' # ── Thème fzf Gruvbox Dark ──────────────────────────────────────── export FZF_DEFAULT_OPTS=" --color=bg+:#3c3836,bg:#282828,spinner:#fb4934,hl:#928374 --color=fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934 --color=marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934 --border=rounded --height=80% --layout=reverse " # ── Icônes ──────────────────────────────────────────────────────── ICO_OK="✓" ICO_UPD="↑" ICO_NEW="+" ICO_NA="·" ICO_LOCAL="●L" ICO_GLOBAL="●G" ICO_SKIP="○" # ── Configuration ───────────────────────────────────────────────── REPO_URL="https://gitea.maison43.duckdns.org/gilles/mes_skills.git" REPO_DIR="/tmp/mes_skills_$$" _CLONED_REPO_DIR="" STATE_FILE="/tmp/skills_state_$$" COLLAPSED_FILE="/tmp/skills_collapsed_$$" SKILLS_DEBUG="${SKILLS_DEBUG:-0}" SKILLS_DRY_RUN="${SKILLS_DRY_RUN:-0}" SKILLS_REPO="${SKILLS_REPO:-}" SKILLS_TAG="${SKILLS_TAG:-}" SKILLS_AGENT="${SKILLS_AGENT:-}" # ── Helpers couleur ─────────────────────────────────────────────── ok() { echo -e "${GRV_GREEN}${ICO_OK} $*${RESET}"; } err() { echo -e "${GRV_RED}✗ $*${RESET}" >&2; } info() { echo -e "${GRV_BLUE}→ $*${RESET}"; } warn() { echo -e "${GRV_ORANGE}⚠ $*${RESET}"; } debug() { [[ "$SKILLS_DEBUG" == "1" ]] && echo -e "${GRV_GRAY}[DBG] $*${RESET}" || true; } header() { echo -e "\n${GRV_PURPLE}╔══ $* ══╗${RESET}\n"; } # ── Nettoyage automatique ───────────────────────────────────────── cleanup() { debug "Nettoyage $_CLONED_REPO_DIR et $STATE_FILE" [[ -n "$_CLONED_REPO_DIR" && -d "$_CLONED_REPO_DIR" ]] && rm -rf "$_CLONED_REPO_DIR" [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" [[ -f "$COLLAPSED_FILE" ]] && rm -f "$COLLAPSED_FILE" } trap cleanup EXIT # ── Installation fzf ────────────────────────────────────────────── _install_fzf_binary() { local tmp_fzf="/tmp/fzf_$$.tar.gz" info "Récupération de la version fzf depuis GitHub API..." local fzf_ver fzf_ver=$(curl -fsSL "https://api.github.com/repos/junegunn/fzf/releases/latest" \ | grep '"tag_name"' | head -1 | awk -F'"' '{print $4}') [[ -z "$fzf_ver" ]] && { err "Impossible de déterminer la version fzf."; exit 1; } local fzf_ver_clean="${fzf_ver#v}" local fzf_url="https://github.com/junegunn/fzf/releases/download/${fzf_ver}/fzf-${fzf_ver_clean}-linux_amd64.tar.gz" info "Téléchargement fzf ${fzf_ver}..." curl -fsSL "$fzf_url" -o "$tmp_fzf" mkdir -p "$HOME/.local/bin" tar -xzf "$tmp_fzf" -C "$HOME/.local/bin/" fzf rm -f "$tmp_fzf" export PATH="$HOME/.local/bin:$PATH" } install_fzf() { warn "fzf non trouvé." echo -e " ${GRV_FG}Installer fzf ? [o/N]${RESET} \c" read -r answer /dev/null; then debug "Installation via apt" apt-get install -y fzf 2>/dev/null || _install_fzf_binary else _install_fzf_binary fi command -v fzf &>/dev/null && ok "fzf installé." || { err "Impossible d'installer fzf."; exit 1; } } # ── Vérification des dépendances ────────────────────────────────── check_deps() { header "Vérification des dépendances" command -v git &>/dev/null || { err "git non trouvé. Installer git et relancer."; exit 1; } ok "git $(git --version | awk '{print $3}')" command -v fzf &>/dev/null || install_fzf ok "fzf $(fzf --version | awk '{print $1}')" } # ── Détection des agents IA ─────────────────────────────────────── DETECTED_AGENTS=() detect_agents() { header "Détection des agents IA" _add_agent() { local name="$1" if [[ -n "$SKILLS_AGENT" && "$SKILLS_AGENT" != "$name" ]]; then debug "Agent $name ignoré (SKILLS_AGENT=$SKILLS_AGENT)" return fi DETECTED_AGENTS+=("$name") ok "Agent détecté : $name" } _skip_agent() { local name="$1" if [[ -z "$SKILLS_AGENT" || "$SKILLS_AGENT" == "$name" ]]; then echo -e "${GRV_GRAY}${ICO_NA} Agent absent : $name${RESET}" fi } _detect_gemini() { command -v gemini &>/dev/null && return 0 local prefix prefix=$(npm config get prefix 2>/dev/null) || return 1 [[ -n "$prefix" && -f "${prefix}/bin/gemini" ]] } # claude-code if [[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null; then _add_agent "claude-code" else _skip_agent "claude-code" fi # gemini-cli if _detect_gemini; then _add_agent "gemini-cli" else _skip_agent "gemini-cli" fi # codex if command -v codex &>/dev/null || [[ -f "$HOME/.npm-global/bin/codex" ]]; then _add_agent "codex" else _skip_agent "codex" fi # hermes if command -v hermes &>/dev/null || [[ -f "$HOME/.local/bin/hermes" ]]; then _add_agent "hermes" else _skip_agent "hermes" fi if [[ ${#DETECTED_AGENTS[@]} -eq 0 ]]; then warn "Aucun agent IA détecté. L'installation continuera mais aucun skill ne sera filtré." fi } # ── Sélection des agents (confirmation après auto-détection) ────── select_agents() { header "Pour quel(s) agent(s) installer ?" local -a agent_lines=() for agent in "claude-code" "gemini-cli" "codex" "hermes"; do local detected=0 for a in "${DETECTED_AGENTS[@]}"; do [[ "$a" == "$agent" ]] && detected=1; done if [[ $detected -eq 1 ]]; then agent_lines+=("${agent}"$'\t'"${GRV_GREEN}✓ détecté ${RESET}${GRV_FG}${agent}${RESET}") else agent_lines+=("${agent}"$'\t'"${GRV_GRAY}○ non installé ${agent}${RESET}") fi done local raw_selected raw_selected=$(printf '%s\n' "${agent_lines[@]}" | fzf \ --multi \ --ansi \ --delimiter='\t' \ --with-nth=2.. \ --prompt="Agents > " \ --header="$(echo -e "${GRV_GRAY}TAB=sélectionner/désélectionner ENTER=valider${RESET}")" \ /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() { 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/^/#/' } # 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" SKILLS_LIST=() scan_skills() { header "Scan des skills disponibles" SKILLS_LIST=() 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#*/}" # 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 ]] && continue # Filtre tag if [[ -n "$SKILLS_TAG" ]]; then grep -q "$SKILLS_TAG" "$skill_file" || continue fi 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}") debug "Skill trouvé : $cat/$skill [$agent] état=$etat" done < <(find "${REPO_DIR}/skills" -name "*.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 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" # 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 "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 TAB : plier/déplier une catégorie cat > "$tab_script" << 'TAB_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 TAB_EOF sed -i "s|FNSFILE|$fns_file|" "$tab_script" chmod +x "$tab_script" # Script LIST : générateur d'arbre pour fzf --reload cat > "$list_script" << 'LIST_EOF' #!/usr/bin/env bash source "FNSFILE" 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]}) 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 done # Footer : skills déjà installés globalement 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" footer_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}') footer_lines+=("d:${agent}|${cat_name}|${skill_name}\t ${GRV_GRAY}${cat_name}/${skill_name} [${agent}] v${ver}${RESET}") done < <(find "${base}/skills" -name "SKILL.md" 2>/dev/null | sort) done if [[ ${#footer_lines[@]} -gt 0 ]]; then printf "d:-\t%s\n" "$(echo -e "${GRV_PURPLE}──── Skills installés globalement ────${RESET}")" for line in "${footer_lines[@]}"; do printf '%s\n' "$line" done 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 _ <<< "$entry" skill_file="${REPO_DIR}/skills/${cat}/${skill}/${agent}.md" 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" == "d" && "$value" != "-" ]]; 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}TAB${RESET}") Plier / déplier la catégorie $(echo -e "${GRV_GREEN}v${RESET}") Afficher / masquer le contenu du skill (preview) $(echo -e "${GRV_GREEN}c${RESET}") Copier la liste affichée dans le presse-papier $(echo -e "${GRV_GREEN}F1${RESET}") Afficher / fermer cette aide $(echo -e "${GRV_BLUE}ARBRE DES CATÉGORIES${RESET}") $(echo -e "${GRV_BLUE}▼${RESET} dev/") Catégorie dépliée — TAB pour replier $(echo -e "${GRV_YELLOW}▶${RESET} infra/") Catégorie repliée — TAB 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 TAB=plier 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="tab:execute-silent(bash $tab_script {1})+reload(bash $list_script)" \ --bind="v:toggle-preview" \ --bind="c:execute-silent(bash $copy_script)" \ --bind="f1:execute(less -R $help_file)" \ < <(bash "$list_script") > /dev/null || true rm -f "$space_script" "$tab_script" "$list_script" "$fns_file" "$preview_script" "$copy_script" "$help_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 IFS='|' read -r cat skill agent etat repo_ver local_ver <<< "$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 src="${REPO_DIR}/skills/${cat}/${skill}/${agent}.md" local dest; dest=$(get_dest_path "$cat" "$skill" "$agent" "$scope") debug "Copie $src → $dest" if [[ "$SKILLS_DRY_RUN" == "1" ]]; then info "[DRY-RUN] cp $src → $dest" else mkdir -p "$(dirname "$dest")" cp "$src" "$dest" 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 "$@"