cc-os/plugins/cc-architect/scripts/validate_marketplaces.rb

167 lines
4.8 KiB
Ruby
Raw Normal View History

#!/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