#!/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 --header-lines=2 " # ── 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_$$" 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" } trap cleanup EXIT # ── Installation fzf ────────────────────────────────────────────── _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" info "Téléchargement fzf depuis GitHub Releases..." 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 [[ "$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 } # ── 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 "\"'" } # 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 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)" } # ── 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 menu ────────────────────────────────────────── format_skill_line() { local entry="$1" 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") 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 "${color_etat}%s${RESET} %-35s ${GRV_GRAY}[%s]${RESET} ${color_action}%s${RESET}%s\n" \ "$ico_etat" "${cat}/${skill}" "$agent" "$ico_action" "$ver_info" } # ── Menu fzf principal ──────────────────────────────────────────── run_menu() { header "Sélection des skills" state_init local cycle_script="/tmp/skills_cycle_$$.sh" local list_script="/tmp/skills_list_$$.sh" local fns_file="/tmp/skills_fns_$$.sh" # Script de cycle d'état (appelé par fzf via execute-silent) cat > "$cycle_script" << 'CYCLE_EOF' #!/usr/bin/env bash STATE_FILE="$1" SKILLS_FNS="$2" LINE_NUM="$3" # numéro de ligne fzf (1-based) 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 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" CYCLE_EOF chmod +x "$cycle_script" # Exporter fonctions et variables dans un fichier source { 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 source "$fns_file" for entry in "\${SKILLS_LIST[@]}"; do format_skill_line "\$entry" done LIST_EOF chmod +x "$list_script" 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}") fzf \ --ansi \ --prompt="Skills > " \ --header="$legend" \ --bind="tab:execute-silent($cycle_script '$STATE_FILE' '$fns_file' {n})+reload($list_script)" \ < <(bash "$list_script") > /dev/null || true rm -f "$cycle_script" "$list_script" "$fns_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 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 "$@"