#!/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 ... # # 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__)} ... Each 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