feat: install.sh — support double format bundle/legacy (amelioration.md)

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 : <agent>.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 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 08:46:12 +02:00
parent 2fe62335c4
commit bdf635e547
2 changed files with 200 additions and 40 deletions
+95 -40
View File
@@ -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 <agent>.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 (<agent>.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
+105
View File
@@ -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 <agent>.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 "══════════════════════════════════"