#!/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_$$" 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 $REPO_DIR et $STATE_FILE" [[ -d "$REPO_DIR" ]] && rm -rf "$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" _check_agent() { local name="$1" primary="$2" secondary="$3" if [[ -n "$SKILLS_AGENT" && "$SKILLS_AGENT" != "$name" ]]; then debug "Agent $name ignoré (SKILLS_AGENT=$SKILLS_AGENT)" return fi if eval "$primary" &>/dev/null 2>&1 || eval "$secondary" &>/dev/null 2>&1; then DETECTED_AGENTS+=("$name") ok "Agent détecté : $name" else echo -e "${GRV_GRAY}${ICO_NA} Agent absent : $name${RESET}" fi } local npm_prefix npm_prefix=$(npm config get prefix 2>/dev/null || echo "") _check_agent "claude-code" "test -d $HOME/.claude" "command -v claude" _check_agent "gemini-cli" "command -v gemini" "test -f ${npm_prefix}/bin/gemini" _check_agent "codex" "command -v codex" "test -f $HOME/.npm-global/bin/codex" _check_agent "hermes" "command -v hermes" "test -f $HOME/.local/bin/hermes" 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 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 for a in "${DETECTED_AGENTS[@]:-}"; do [[ "$a" == "$agent" ]] && agent_detected=1 done [[ "$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)" }