From bdf635e5479b340ad2ab1dc8ffdf42aba240dd2f Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 16 May 2026 08:46:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20install.sh=20=E2=80=94=20support=20doub?= =?UTF-8?q?le=20format=20bundle/legacy=20(amelioration.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double détection dans scan_skills() : - Bundle : dossier avec SKILL.md, agents: lu dans le frontmatter, copie récursive du dossier complet (scripts/, templates/, references/) - Legacy : .md existant, agent déduit du nom de fichier - Priorité bundle sur legacy pour un même cat/skill/agent - Nouveau champ get_frontmatter_agents() pour parser agents: [...] - SKILLS_LIST étendu : ...|kind|source_path (10 champs) - install_selected() branche sur kind=bundle vs legacy - preview_script utilise source_path pour trouver le fichier à afficher ha-log-investigator (bundle avec scripts/ et references/) est maintenant détecté et installé correctement. Tests : section 5 ajoutée — 9 nouveaux cas (bundle, legacy, références ignorées, doublon, priorité, source_path, accessibilité fichiers). Co-Authored-By: Claude Sonnet 4.6 --- install.sh | 135 +++++++++++++++++++++++++++++------------- tests/test_install.sh | 105 ++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 40 deletions(-) diff --git a/install.sh b/install.sh index 046870a..6872c55 100755 --- a/install.sh +++ b/install.sh @@ -234,6 +234,12 @@ get_frontmatter_tags() { | sed 's/^tags:[[:space:]]*//' | tr -d '[] ' | tr ',' '#' | sed 's/^/#/' } +# Parse "agents: [claude-code, codex]" → une ligne par agent +get_frontmatter_agents() { + grep "^agents:" "$1" 2>/dev/null | head -1 \ + | sed 's/^agents:[[:space:]]*//' | tr -d '[]' | tr ',' '\n' | tr -d ' ' | grep -v '^$' +} + # Retourne 0 (succès) si ver2 est plus récente que ver1 version_is_newer() { local ver1="$1" ver2="$2" @@ -264,13 +270,65 @@ get_local_version() { } # ── Scan des skills disponibles ─────────────────────────────────── -# Format : "cat|skill|agent|etat|repo_version|local_version" +# Format : "cat|skill|agent|etat|repo_ver|local_ver|desc|tags|kind|source_path" +# kind=bundle → dossier SKILL.md+ressources | kind=legacy → fichier .md SKILLS_LIST=() +_skill_agent_ok() { + local agent="$1" + [[ ${#DETECTED_AGENTS[@]} -eq 0 ]] && return 0 + for a in "${DETECTED_AGENTS[@]}"; do [[ "$a" == "$agent" ]] && return 0; done + return 1 +} + +_build_entry() { + local cat="$1" skill="$2" agent="$3" kind="$4" source_path="$5" skill_file="$6" + 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") + echo "${cat}|${skill}|${agent}|${etat}|${repo_ver}|${local_ver}|${desc}|${tags}|${kind}|${source_path}" +} + scan_skills() { header "Scan des skills disponibles" SKILLS_LIST=() + declare -A seen_combos + # ── Chemin 1 : bundles (dossier avec SKILL.md, agents: dans le frontmatter) + while IFS= read -r skill_file; do + local rel="${skill_file#${REPO_DIR}/skills/}" + local skill_dir="${rel%/SKILL.md}" + local cat="${skill_dir%%/*}" + local skill="${skill_dir#*/}" + local source_dir="${REPO_DIR}/skills/${cat}/${skill}" + + local raw_agents; raw_agents=$(get_frontmatter_agents "$skill_file") + if [[ -z "$raw_agents" ]]; then + debug "Bundle sans agents: ignoré : $cat/$skill" + continue + fi + + while IFS= read -r agent; do + _skill_agent_ok "$agent" || continue + [[ -n "$SKILLS_TAG" ]] && { grep -q "$SKILLS_TAG" "$skill_file" || continue; } + local entry; entry=$(_build_entry "$cat" "$skill" "$agent" "bundle" "$source_dir" "$skill_file") + SKILLS_LIST+=("$entry") + seen_combos["${cat}|${skill}|${agent}"]=1 + debug "Bundle : $cat/$skill [$agent]" + done <<< "$raw_agents" + done < <(find "${REPO_DIR}/skills" -name "SKILL.md" | sort) + + # ── Chemin 2 : legacy (.md — agents connus uniquement) + local known_agents="claude-code gemini-cli codex hermes" while IFS= read -r skill_file; do local rel="${skill_file#${REPO_DIR}/skills/}" local agent_file="${rel##*/}" @@ -279,37 +337,21 @@ scan_skills() { 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 + # Ignorer les fichiers qui ne correspondent pas à un agent connu + local is_known=0 + for ka in $known_agents; do [[ "$ka" == "$agent" ]] && is_known=1; done + [[ "$is_known" -eq 0 ]] && continue - # Filtre tag - if [[ -n "$SKILLS_TAG" ]]; then - grep -q "$SKILLS_TAG" "$skill_file" || continue - fi + # Le bundle a priorité : ignorer si déjà vu + [[ -n "${seen_combos[${cat}|${skill}|${agent}]:-}" ]] && continue - local repo_ver; repo_ver=$(get_frontmatter_field "$skill_file" "version") - local local_ver; local_ver=$(get_local_version "$cat" "$skill" "$agent") - local etat + _skill_agent_ok "$agent" || continue + [[ -n "$SKILLS_TAG" ]] && { grep -q "$SKILLS_TAG" "$skill_file" || continue; } - 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) + local entry; entry=$(_build_entry "$cat" "$skill" "$agent" "legacy" "$skill_file" "$skill_file") + SKILLS_LIST+=("$entry") + debug "Legacy : $cat/$skill [$agent]" + done < <(find "${REPO_DIR}/skills" -name "*.md" ! -name "SKILL.md" | sort) ok "${#SKILLS_LIST[@]} skill(s) trouvé(s)" } @@ -588,8 +630,12 @@ 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" + IFS='|' read -r cat skill agent _ _ _ _ _ kind source_path <<< "$entry" + if [[ "$kind" == "bundle" ]]; then + skill_file="${source_path}/SKILL.md" + else + skill_file="$source_path" + fi 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" @@ -733,8 +779,8 @@ install_selected() { 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 cat skill agent etat repo_ver local_ver desc tags kind source_path + IFS='|' read -r cat skill agent etat repo_ver local_ver desc tags kind source_path <<< "$entry" local key; key=$(make_key "$entry") local action; action=$(state_get "$key") @@ -746,16 +792,25 @@ install_selected() { 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" + if [[ "$kind" == "bundle" ]]; then + local dest_dir; dest_dir="$(dirname "$dest")" + debug "Bundle $source_path/ → $dest_dir/" + if [[ "$SKILLS_DRY_RUN" == "1" ]]; then + info "[DRY-RUN] cp -r $source_path/ → $dest_dir/" + else + mkdir -p "$dest_dir" + cp -r "${source_path}/." "$dest_dir/" + fi else - mkdir -p "$(dirname "$dest")" - cp "$src" "$dest" + debug "Legacy $source_path → $dest" + if [[ "$SKILLS_DRY_RUN" == "1" ]]; then + info "[DRY-RUN] cp $source_path → $dest" + else + mkdir -p "$(dirname "$dest")" + cp "$source_path" "$dest" + fi fi if [[ "$action" == "update" ]]; then diff --git a/tests/test_install.sh b/tests/test_install.sh index a433db3..4d1e17d 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -161,6 +161,111 @@ assert_eq "cycle update→local" "local" "$(state_get "dev_debugging_claude_code rm -f "$STATE_FILE" unset STATE_FILE || true +# ── 5. Détection bundle / legacy ───────────────────────────────── +echo "" +echo "5. get_frontmatter_agents() / scan_skills() double format" + +TMP_DIR=$(mktemp -d) + +# Skill bundle : SKILL.md + agents: [claude-code, codex] +mkdir -p "$TMP_DIR/skills/infra/mon-bundle" +cat > "$TMP_DIR/skills/infra/mon-bundle/SKILL.md" << 'EOF' +--- +name: mon-bundle +version: 1.2.0 +agents: [claude-code, codex] +description: Skill bundle test +tags: [test] +--- +# Mon bundle +EOF +mkdir -p "$TMP_DIR/skills/infra/mon-bundle/scripts" +touch "$TMP_DIR/skills/infra/mon-bundle/scripts/helper.sh" + +# Skill legacy : claude-code.md +mkdir -p "$TMP_DIR/skills/dev/ancien" +cat > "$TMP_DIR/skills/dev/ancien/claude-code.md" << 'EOF' +--- +name: ancien +version: 0.5.0 +description: Skill legacy +tags: [legacy] +--- +# Ancien skill +EOF + +# Fichier Markdown de support (ne doit PAS être détecté comme skill) +mkdir -p "$TMP_DIR/skills/infra/mon-bundle/references" +echo "# Ref doc" > "$TMP_DIR/skills/infra/mon-bundle/references/notes.md" + +assert_eq "agents: [claude-code, codex] → 2 agents" \ + "claude-code +codex" \ + "$(get_frontmatter_agents "$TMP_DIR/skills/infra/mon-bundle/SKILL.md")" + +assert_eq "agents: absent → vide" \ + "" \ + "$(get_frontmatter_agents "$TMP_DIR/skills/dev/ancien/claude-code.md")" + +# scan_skills avec les deux formats +REPO_DIR="$TMP_DIR" +DETECTED_AGENTS=() +SKILLS_TAG="" +STATE_FILE=$(mktemp) +scan_skills 2>/dev/null + +# Compter les entrées trouvées +bundle_count=0; legacy_count=0; ref_count=0 +for e in "${SKILLS_LIST[@]}"; do + IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e" + [[ "$kind" == "bundle" ]] && (( bundle_count++ )) || true + [[ "$kind" == "legacy" ]] && (( legacy_count++ )) || true + # Vérifier qu'aucune entrée ne vient de references/notes.md + [[ "$source_path" == *"notes.md"* ]] && (( ref_count++ )) || true +done + +assert_eq "bundle détecté 2 fois (2 agents)" "2" "$bundle_count" +assert_eq "legacy détecté 1 fois" "1" "$legacy_count" +assert_eq "fichier references/ ignoré" "0" "$ref_count" + +# Vérifier que kind=bundle et source_path pointent sur le dossier +first_bundle="" +for e in "${SKILLS_LIST[@]}"; do + IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e" + [[ "$kind" == "bundle" ]] && { first_bundle="$source_path"; break; } +done +assert_eq "source_path bundle = dossier du skill" \ + "$TMP_DIR/skills/infra/mon-bundle" \ + "$first_bundle" + +# Vérifier que SKILL.md du bundle est lisible pour le preview +assert_true "SKILL.md bundle accessible" "[[ -f '${first_bundle}/SKILL.md' ]]" +assert_true "scripts/ du bundle accessible" "[[ -f '${first_bundle}/scripts/helper.sh' ]]" + +# Priorité bundle sur legacy : si SKILL.md existe, le .md ne duplique pas +mkdir -p "$TMP_DIR/skills/dev/ancien" +cat > "$TMP_DIR/skills/dev/ancien/SKILL.md" << 'EOF' +--- +name: ancien +version: 1.0.0 +agents: [claude-code] +description: Skill migré en bundle +tags: [test] +--- +# Ancien migré +EOF +SKILLS_LIST=() +scan_skills 2>/dev/null +count_ancien=0 +for e in "${SKILLS_LIST[@]}"; do + [[ "$e" == *"|ancien|claude-code|"* ]] && (( count_ancien++ )) || true +done +assert_eq "pas de doublon bundle+legacy pour même agent" "1" "$count_ancien" + +rm -f "$STATE_FILE" +rm -rf "$TMP_DIR" +unset REPO_DIR STATE_FILE SKILLS_TAG DETECTED_AGENTS + # ── Bilan ───────────────────────────────────────────────────────── echo "" echo "══════════════════════════════════"