167 lines
4.8 KiB
Ruby
167 lines
4.8 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# Cross-repo marketplace acceptance script.
|
|
#
|
|
# For one or more marketplace roots (repos containing a
|
|
# .claude-plugin/marketplace.json), runs the plugin_config validate
|
|
# operation against every plugin the marketplace declares and reports a
|
|
# pass/fail table plus totals.
|
|
#
|
|
# Usage:
|
|
# ruby validate_marketplaces.rb <marketplace-root>...
|
|
#
|
|
# With no arguments, prints usage (including the documented default
|
|
# invocation for this machine) and exits 2.
|
|
#
|
|
# Exit status:
|
|
# 0 every plugin in every given marketplace passed validation
|
|
# 1 at least one plugin failed validation
|
|
# 2 usage error (no args, missing/invalid marketplace.json, etc.)
|
|
|
|
require 'json'
|
|
require 'open3'
|
|
|
|
SCRIPT_DIR = File.expand_path(__dir__)
|
|
PLUGIN_CONFIG_RB = File.join(SCRIPT_DIR, 'plugin_config', 'plugin_config.rb')
|
|
|
|
DEFAULT_MARKETPLACE_ROOTS = [
|
|
'/home/jared/dev/cc-plugins',
|
|
'/home/jared/dev/cc-os'
|
|
].freeze
|
|
|
|
def usage
|
|
<<~USAGE
|
|
Usage: ruby #{File.basename(__FILE__)} <marketplace-root>...
|
|
|
|
Each <marketplace-root> is a repo root containing
|
|
.claude-plugin/marketplace.json. Every plugin declared in that
|
|
marketplace is validated with the plugin_config `validate` operation,
|
|
against that marketplace's manifest.
|
|
|
|
Documented default invocation for this machine:
|
|
ruby #{__FILE__} #{DEFAULT_MARKETPLACE_ROOTS.join(' ')}
|
|
USAGE
|
|
end
|
|
|
|
# Runs `plugin_config.rb validate` for a single plugin and returns the
|
|
# parsed JSON result (or a synthetic failure result if the subprocess
|
|
# couldn't be run or produced unparseable output).
|
|
def validate_plugin(plugin_path, marketplace_file)
|
|
cmd = [
|
|
'ruby', PLUGIN_CONFIG_RB, 'validate',
|
|
"--plugin_path=#{plugin_path}",
|
|
"--marketplace_file=#{marketplace_file}"
|
|
]
|
|
stdout, stderr, _status = Open3.capture3(*cmd)
|
|
|
|
begin
|
|
JSON.parse(stdout, symbolize_names: true)
|
|
rescue JSON::ParserError
|
|
{
|
|
status: 'issues_found',
|
|
valid: false,
|
|
plugin_path: plugin_path,
|
|
checks: {},
|
|
issues: ["validator produced unparseable output: #{stdout.strip}#{stderr.strip}"]
|
|
}
|
|
end
|
|
end
|
|
|
|
# Validates every plugin declared by one marketplace root. Returns
|
|
# { root:, plugin_results: [{name:, plugin_path:, valid:, issues: []}], error: nil|String }
|
|
def validate_marketplace(root)
|
|
abs_root = File.expand_path(root)
|
|
marketplace_file = File.join(abs_root, '.claude-plugin', 'marketplace.json')
|
|
|
|
unless File.exist?(marketplace_file)
|
|
return { root: abs_root, marketplace_file: marketplace_file, plugin_results: [], error: "marketplace.json not found at #{marketplace_file}" }
|
|
end
|
|
|
|
data = begin
|
|
JSON.parse(File.read(marketplace_file))
|
|
rescue JSON::ParserError => e
|
|
return { root: abs_root, marketplace_file: marketplace_file, plugin_results: [], error: "invalid JSON in #{marketplace_file}: #{e.message}" }
|
|
end
|
|
|
|
plugins = data['plugins'] || []
|
|
if plugins.empty?
|
|
return { root: abs_root, marketplace_file: marketplace_file, plugin_results: [], error: "no plugins declared in #{marketplace_file}" }
|
|
end
|
|
|
|
plugin_results = plugins.map do |entry|
|
|
name = entry['name']
|
|
source = entry['source']
|
|
plugin_path = source ? File.expand_path(source, abs_root) : nil
|
|
|
|
if plugin_path.nil?
|
|
{ name: name || '(unnamed)', plugin_path: nil, valid: false, issues: ['marketplace entry has no source field'] }
|
|
else
|
|
result = validate_plugin(plugin_path, marketplace_file)
|
|
{
|
|
name: name || '(unnamed)',
|
|
plugin_path: plugin_path,
|
|
valid: result[:valid] == true,
|
|
issues: Array(result[:issues])
|
|
}
|
|
end
|
|
end
|
|
|
|
{ root: abs_root, marketplace_file: marketplace_file, plugin_results: plugin_results, error: nil }
|
|
end
|
|
|
|
def print_report(marketplace_reports)
|
|
total_plugins = 0
|
|
total_failed = 0
|
|
|
|
marketplace_reports.each do |report|
|
|
puts "== #{report[:root]} =="
|
|
|
|
if report[:error]
|
|
puts " ERROR: #{report[:error]}"
|
|
puts
|
|
next
|
|
end
|
|
|
|
report[:plugin_results].each do |pr|
|
|
total_plugins += 1
|
|
if pr[:valid]
|
|
puts " PASS #{pr[:name]}"
|
|
else
|
|
total_failed += 1
|
|
first_issue = pr[:issues].first
|
|
summary = first_issue ? first_issue.to_s.lines.first.to_s.strip : '(no issue detail)'
|
|
puts " FAIL #{pr[:name]} - #{summary}"
|
|
end
|
|
end
|
|
puts
|
|
end
|
|
|
|
[total_plugins, total_failed]
|
|
end
|
|
|
|
def main(argv)
|
|
if argv.empty?
|
|
puts usage
|
|
exit 2
|
|
end
|
|
|
|
marketplace_reports = argv.map { |root| validate_marketplace(root) }
|
|
|
|
total_plugins, total_failed = print_report(marketplace_reports)
|
|
|
|
hard_error = marketplace_reports.any? { |r| r[:error] }
|
|
|
|
puts "TOTAL: #{total_plugins} plugin(s) checked, #{total_plugins - total_failed} passed, #{total_failed} failed"
|
|
|
|
if hard_error
|
|
exit 2
|
|
elsif total_failed.positive?
|
|
exit 1
|
|
else
|
|
exit 0
|
|
end
|
|
end
|
|
|
|
main(ARGV) if __FILE__ == $PROGRAM_NAME
|