225 lines
8.9 KiB
Bash
Executable File
225 lines
8.9 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_$$"
|
|
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)"
|
|
}
|