Files
gh-lerianstudio-ring-default/hooks/generate-skills-ref.sh
2025-11-30 08:37:11 +08:00

208 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Fallback skill reference generator when Python is unavailable
# Requires bash 3.2+ (uses [[ ]], ${BASH_SOURCE}, ${var:0:n})
# Tools used: sed, awk, grep (standard on macOS/Linux/Git Bash)
#
# This script provides a degraded but functional skills quick reference
# when Python or PyYAML are not available on the system.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SKILLS_DIR="${PLUGIN_ROOT}/skills"
# Parse a single field from YAML frontmatter
# Uses the proven sed pattern from ralph-wiggum/hooks/stop-hook.sh
extract_field() {
local frontmatter="$1"
local field="$2"
# For simple fields: fieldname: value
# For block scalars: fieldname: | followed by indented lines
echo "$frontmatter" | awk -v field="$field" '
BEGIN { found = 0; value = "" }
# Match the field we want
$0 ~ "^" field ":" {
found = 1
# Check for inline value (not block scalar)
sub("^" field ":[[:space:]]*\\|?[[:space:]]*", "")
if (length($0) > 0 && $0 !~ /^\|[[:space:]]*$/) {
value = $0
exit
}
next
}
# If we found our field and this line is indented, capture it
found && /^[[:space:]]+[^[:space:]]/ {
gsub(/^[[:space:]]+/, "")
gsub(/[[:space:]]+$/, "")
# Skip list markers for cleaner output
gsub(/^-[[:space:]]+/, "")
if (length($0) > 0 && value == "") {
value = $0
exit
}
}
# If we hit another field definition, stop
found && /^[a-z_]+:/ && $0 !~ "^" field ":" {
exit
}
END { print value }
'
}
# Parse YAML frontmatter from SKILL.md
parse_skill() {
local skill_file="$1"
local skill_dir
skill_dir=$(basename "$(dirname "$skill_file")")
# Skip shared-patterns directory
if [[ "$skill_dir" == "shared-patterns" ]]; then
return
fi
# Extract frontmatter between --- delimiters
# Pattern proven portable in ralph-wiggum/hooks/stop-hook.sh
local frontmatter
frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$skill_file" 2>/dev/null) || return
if [[ -z "$frontmatter" ]]; then
echo "Warning: No frontmatter in $skill_file" >&2
return
fi
# Extract fields
local name description trigger
name=$(extract_field "$frontmatter" "name")
description=$(extract_field "$frontmatter" "description")
trigger=$(extract_field "$frontmatter" "trigger")
# Fallback: use when_to_use if trigger not set (backward compat)
if [[ -z "$trigger" ]]; then
trigger=$(extract_field "$frontmatter" "when_to_use")
fi
# Use directory name if name field missing
if [[ -z "$name" ]]; then
name="$skill_dir"
fi
# Default description if missing
if [[ -z "$description" ]]; then
description="(no description)"
fi
# Truncate long descriptions for quick reference
if [[ ${#description} -gt 100 ]]; then
description="${description:0:97}..."
fi
# Output as TSV for reliable parsing (dir, name, description, trigger)
printf '%s\t%s\t%s\t%s\n' "$skill_dir" "$name" "$description" "$trigger"
}
# Categorize skill based on directory name
categorize_skill() {
local dir="$1"
case "$dir" in
pre-dev-*) echo "Pre-Dev Workflow" ;;
test-*|*-debugging|condition-*|defense-*|root-cause*) echo "Testing & Debugging" ;;
*-review|dispatching-*|sharing-*) echo "Collaboration" ;;
brainstorming|writing-plans|executing-plans|*-worktrees|subagent-driven*) echo "Planning & Execution" ;;
using-*|writing-skills|testing-skills*|testing-agents*) echo "Meta Skills" ;;
*) echo "Other" ;;
esac
}
# Generate markdown output
generate_markdown() {
echo "# Ring Skills Quick Reference"
echo ""
echo "> **Note:** Python unavailable. Using bash fallback parser."
echo "> Install Python + PyYAML for full output with categories."
echo ""
local skill_count=0
local current_category=""
# Sort by category, then by name
while IFS=$'\t' read -r dir name desc trigger; do
local category
category=$(categorize_skill "$dir")
# Print category header if changed
if [[ "$category" != "$current_category" ]]; then
if [[ -n "$current_category" ]]; then
echo ""
fi
echo "## $category"
echo ""
current_category="$category"
fi
# Combine description with trigger hint if available
local display_desc="$desc"
if [[ -n "$trigger" && "$trigger" != "$desc" ]]; then
display_desc="$trigger"
fi
echo "- **${name}**: ${display_desc}"
skill_count=$((skill_count + 1))
done
echo ""
echo "## Usage"
echo ""
echo "To use a skill: Use the Skill tool with skill name"
echo "Example: \`ring-default:brainstorming\`"
# Output stats to stderr (like Python version)
echo "" >&2
echo "Generated reference for ${skill_count} skills (bash fallback)" >&2
}
# Main execution
main() {
if [[ ! -d "$SKILLS_DIR" ]]; then
echo "Error: Skills directory not found: $SKILLS_DIR" >&2
exit 1
fi
# Collect all skills with categories, then sort and generate markdown
local tmpfile
tmpfile=$(mktemp)
chmod 600 "$tmpfile" # Restrict permissions for security
trap "rm -f '$tmpfile'" EXIT INT TERM HUP
for skill_dir in "$SKILLS_DIR"/*/; do
# Skip if not a directory
[[ -d "$skill_dir" ]] || continue
local skill_file="${skill_dir}SKILL.md"
if [[ -f "$skill_file" ]]; then
local skill_line
skill_line=$(parse_skill "$skill_file")
if [[ -n "$skill_line" ]]; then
# Add category as first field for sorting
local dir name desc trigger cat
IFS=$'\t' read -r dir name desc trigger <<< "$skill_line"
cat=$(categorize_skill "$dir")
printf '%s\t%s\t%s\t%s\t%s\n' "$cat" "$dir" "$name" "$desc" "$trigger" >> "$tmpfile"
fi
else
echo "Warning: No SKILL.md in $(basename "$skill_dir")" >&2
fi
done
# Sort by category, then by name, remove category column, generate markdown
sort -t$'\t' -k1,1 -k3,3 "$tmpfile" | cut -f2- | generate_markdown
}
main "$@"