mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
cde4a67ef4
Generate a root discovery catalog from public skill frontmatter and fail CI when the committed index drifts. Add a fixture-based regression test for bundle paths, nested-helper exclusion, normalized descriptions, deterministic ordering, and stale-file recovery. Closes #78 AI-assisted: yes (Jasper/Hermes Agent) Co-authored-by: magnus919 <magnus919>
108 lines
3.4 KiB
Ruby
108 lines
3.4 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# Generates and validates root llms.txt catalog of all public installable skills:
|
|
#
|
|
# # agent-skills
|
|
#
|
|
# ## Skills
|
|
#
|
|
# - [name](relative/path/SKILL.md): Description
|
|
#
|
|
# Each skill entry appears exactly once, keyed by its skill directory name, with a one-line description.
|
|
# Description normalization: folded/multiline YAML frontmatter becomes a single space-separated line.
|
|
# Links are relative to repository root per issue #78.
|
|
#
|
|
# Usage:
|
|
# ruby scripts/gen-llms-txt.rb # validate committed llms.txt matches current
|
|
# ruby scripts/gen-llms-txt.rb --write # regenerate llms.txt and report count
|
|
|
|
require "fileutils"
|
|
require "yaml"
|
|
|
|
ROOT = File.expand_path("..", __dir__)
|
|
LLMS_TXT_PATH = File.join(ROOT, "llms.txt")
|
|
|
|
# Public install candidates: top-level skills and bundle entrypoints.
|
|
# Same glob as gen-claude-marketplace.rb, gen-codex-plugin.rb, validate-skills.rb and README catalog:
|
|
# #{ROOT}/*/SKILL.md + #{ROOT}/bundles/*/SKILL.md
|
|
# Excludes bundle-internal helper skills (bundles/<x>/skills/<sub>/SKILL.md).
|
|
PUBLIC_SKILLS = (Dir.glob("#{ROOT}/*/SKILL.md") + Dir.glob("#{ROOT}/bundles/*/SKILL.md")).sort
|
|
|
|
def frontmatter(skill_path)
|
|
text = File.read(skill_path)
|
|
match = text.match(/\A---\n(.*?)\n---\n/m)
|
|
raise "#{skill_path}: missing YAML frontmatter" unless match
|
|
|
|
data = YAML.safe_load(match[1], permitted_classes: [], aliases: false)
|
|
raise "#{skill_path}: frontmatter must be a mapping" unless data.is_a?(Hash)
|
|
|
|
data
|
|
end
|
|
|
|
def normalize_description(text)
|
|
text.to_s.gsub(/\s+/, " ").strip
|
|
end
|
|
|
|
def build_llms_txt
|
|
skill_entries = []
|
|
seen_names = {}
|
|
|
|
PUBLIC_SKILLS.sort_by do |path|
|
|
name = File.basename(File.dirname(path))
|
|
[name.downcase, name, path.delete_prefix("#{ROOT}/")]
|
|
end.each do |skill_path|
|
|
dir = File.dirname(skill_path)
|
|
name = File.basename(dir)
|
|
data = frontmatter(skill_path)
|
|
relative_path = skill_path.delete_prefix("#{ROOT}/")
|
|
raw_name = data["name"]
|
|
unless raw_name.is_a?(String) && raw_name == name
|
|
raise "#{relative_path}: name must match directory name #{name.inspect}"
|
|
end
|
|
name_key = name.downcase
|
|
if seen_names.key?(name_key)
|
|
raise "duplicate catalog name #{name.inspect}: #{seen_names[name_key]} and #{relative_path}"
|
|
end
|
|
seen_names[name_key] = relative_path
|
|
|
|
raw_description = data["description"]
|
|
unless raw_description.is_a?(String) && !raw_description.strip.empty?
|
|
raise "#{relative_path}: description must be a nonempty string"
|
|
end
|
|
description = normalize_description(raw_description)
|
|
|
|
entry = "- [#{name}](#{relative_path}): #{description}"
|
|
skill_entries << entry
|
|
end
|
|
|
|
result = ["# agent-skills", "", "## Skills", "", *skill_entries, ""].join("\n")
|
|
result
|
|
end
|
|
|
|
def write_llms_txt(content)
|
|
FileUtils.mkdir_p(File.dirname(LLMS_TXT_PATH))
|
|
File.write(LLMS_TXT_PATH, content)
|
|
puts "Wrote #{LLMS_TXT_PATH.delete_prefix("#{ROOT}/")} (#{PUBLIC_SKILLS.length} skills)."
|
|
end
|
|
|
|
expected = build_llms_txt
|
|
|
|
if ARGV.include?("--write")
|
|
write_llms_txt(expected)
|
|
exit 0
|
|
end
|
|
|
|
unless File.file?(LLMS_TXT_PATH)
|
|
warn "#{LLMS_TXT_PATH.delete_prefix("#{ROOT}/")} is missing. Run: ruby scripts/gen-llms-txt.rb --write"
|
|
exit 1
|
|
end
|
|
|
|
actual = File.read(LLMS_TXT_PATH)
|
|
if actual != expected
|
|
warn "#{LLMS_TXT_PATH.delete_prefix("#{ROOT}/")} is out of date. Run: ruby scripts/gen-llms-txt.rb --write"
|
|
exit 1
|
|
end
|
|
|
|
puts "llms.txt is current (#{PUBLIC_SKILLS.length} skills)."
|