Files
mes_skills/install.sh
T

507 lines
19 KiB
Bash
Executable File

#!/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 "$@"