#!/usr/bin/env python3 """ Marketplace Update Script Updates the .claude-plugin/marketplace.json file when plugins are added, modified, or removed. """ import json import os import sys from pathlib import Path from typing import Dict, List, Optional, Any import argparse class MarketplaceUpdater: """Handles marketplace.json updates""" def __init__(self, marketplace_path: str = ".claude-plugin/marketplace.json"): self.marketplace_path = Path(marketplace_path) self.marketplace_data: Dict[str, Any] = {} def load(self) -> None: """Load marketplace.json file""" if not self.marketplace_path.exists(): raise FileNotFoundError(f"Marketplace file not found: {self.marketplace_path}") try: with open(self.marketplace_path, 'r') as f: self.marketplace_data = json.load(f) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON in marketplace file: {e}") def save(self) -> None: """Save marketplace.json file""" with open(self.marketplace_path, 'w') as f: json.dump(self.marketplace_data, f, indent=2) f.write('\n') # Add trailing newline def add_plugin( self, name: str, description: str, version: str, category: str = "general", agents: Optional[List[str]] = None, commands: Optional[List[str]] = None, skills: Optional[List[str]] = None, keywords: Optional[List[str]] = None, license: str = "MIT", strict: bool = False, author_name: Optional[str] = None, author_url: Optional[str] = None, ) -> None: """Add a new plugin to the marketplace""" self.load() # Check if plugin already exists if "plugins" not in self.marketplace_data: self.marketplace_data["plugins"] = [] existing_plugin = self._find_plugin(name) if existing_plugin: raise ValueError(f"Plugin '{name}' already exists in marketplace") # Build plugin entry plugin_entry: Dict[str, Any] = { "name": name, "source": f"./plugins/{name}", "description": description, "version": version, "category": category, "license": license, "strict": strict, } # Add author if provided, otherwise use marketplace owner if author_name or author_url: plugin_entry["author"] = {} if author_name: plugin_entry["author"]["name"] = author_name if author_url: plugin_entry["author"]["url"] = author_url elif "owner" in self.marketplace_data: plugin_entry["author"] = { "name": self.marketplace_data["owner"].get("name", ""), "url": self.marketplace_data["owner"].get("url", ""), } # Add homepage and repository from owner if available if "owner" in self.marketplace_data and "url" in self.marketplace_data["owner"]: base_url = self.marketplace_data["owner"]["url"] # Extract repo name from URL if it's a GitHub URL if "github.com" in base_url: plugin_entry["homepage"] = base_url plugin_entry["repository"] = base_url # Add optional fields if keywords: plugin_entry["keywords"] = keywords if agents: plugin_entry["agents"] = [f"./agents/{agent}" for agent in agents] if commands: plugin_entry["commands"] = [f"./commands/{cmd}" for cmd in commands] if skills: plugin_entry["skills"] = [f"./skills/{skill}" for skill in skills] # Add plugin to marketplace self.marketplace_data["plugins"].append(plugin_entry) self.save() print(f"✓ Added plugin '{name}' to marketplace") def update_plugin( self, name: str, description: Optional[str] = None, version: Optional[str] = None, category: Optional[str] = None, keywords: Optional[List[str]] = None, add_agent: Optional[str] = None, remove_agent: Optional[str] = None, add_command: Optional[str] = None, remove_command: Optional[str] = None, add_skill: Optional[str] = None, remove_skill: Optional[str] = None, ) -> None: """Update an existing plugin""" self.load() plugin = self._find_plugin(name) if not plugin: raise ValueError(f"Plugin '{name}' not found in marketplace") # Update basic fields if description: plugin["description"] = description if version: plugin["version"] = version if category: plugin["category"] = category if keywords: plugin["keywords"] = keywords # Update agents if add_agent: if "agents" not in plugin: plugin["agents"] = [] agent_path = f"./agents/{add_agent}" if agent_path not in plugin["agents"]: plugin["agents"].append(agent_path) if remove_agent: if "agents" in plugin: agent_path = f"./agents/{remove_agent}" if agent_path in plugin["agents"]: plugin["agents"].remove(agent_path) # Update commands if add_command: if "commands" not in plugin: plugin["commands"] = [] cmd_path = f"./commands/{add_command}" if cmd_path not in plugin["commands"]: plugin["commands"].append(cmd_path) if remove_command: if "commands" in plugin: cmd_path = f"./commands/{remove_command}" if cmd_path in plugin["commands"]: plugin["commands"].remove(cmd_path) # Update skills if add_skill: if "skills" not in plugin: plugin["skills"] = [] skill_path = f"./skills/{add_skill}" if skill_path not in plugin["skills"]: plugin["skills"].append(skill_path) if remove_skill: if "skills" in plugin: skill_path = f"./skills/{remove_skill}" if skill_path in plugin["skills"]: plugin["skills"].remove(skill_path) self.save() print(f"✓ Updated plugin '{name}' in marketplace") def remove_plugin(self, name: str) -> None: """Remove a plugin from the marketplace""" self.load() plugin = self._find_plugin(name) if not plugin: raise ValueError(f"Plugin '{name}' not found in marketplace") self.marketplace_data["plugins"].remove(plugin) self.save() print(f"✓ Removed plugin '{name}' from marketplace") def validate(self) -> bool: """Validate marketplace structure""" self.load() errors = [] warnings = [] # Check required top-level fields required_fields = ["name", "owner", "metadata", "plugins"] for field in required_fields: if field not in self.marketplace_data: errors.append(f"Missing required field: {field}") # Validate plugins if "plugins" in self.marketplace_data: plugin_names = set() for i, plugin in enumerate(self.marketplace_data["plugins"]): # Check required plugin fields plugin_required = ["name", "source", "description", "version"] for field in plugin_required: if field not in plugin: errors.append(f"Plugin {i}: Missing required field '{field}'") # Check for duplicate names if "name" in plugin: if plugin["name"] in plugin_names: errors.append(f"Duplicate plugin name: {plugin['name']}") plugin_names.add(plugin["name"]) # Validate component file paths plugin_dir = Path(f"plugins/{plugin['name']}") if "agents" in plugin: for agent in plugin["agents"]: agent_path = plugin_dir / agent if not agent_path.exists(): warnings.append( f"Plugin '{plugin['name']}': Agent file not found: {agent_path}" ) if "commands" in plugin: for command in plugin["commands"]: cmd_path = plugin_dir / command if not cmd_path.exists(): warnings.append( f"Plugin '{plugin['name']}': Command file not found: {cmd_path}" ) if "skills" in plugin: for skill in plugin["skills"]: skill_path = plugin_dir / skill / "SKILL.md" if not skill_path.exists(): warnings.append( f"Plugin '{plugin['name']}': Skill file not found: {skill_path}" ) # Report results if errors: print("❌ Validation failed with errors:") for error in errors: print(f" - {error}") else: print("✓ Validation passed with no errors") if warnings: print("\n⚠️ Warnings:") for warning in warnings: print(f" - {warning}") return len(errors) == 0 def _find_plugin(self, name: str) -> Optional[Dict[str, Any]]: """Find a plugin by name""" if "plugins" not in self.marketplace_data: return None for plugin in self.marketplace_data["plugins"]: if plugin.get("name") == name: return plugin return None def main(): """Main entry point""" parser = argparse.ArgumentParser(description="Update marketplace.json") subparsers = parser.add_subparsers(dest="command", help="Command to execute") # Add command add_parser = subparsers.add_parser("add", help="Add a new plugin") add_parser.add_argument("--name", required=True, help="Plugin name") add_parser.add_argument("--description", required=True, help="Plugin description") add_parser.add_argument("--version", required=True, help="Plugin version") add_parser.add_argument("--category", default="general", help="Plugin category") add_parser.add_argument("--agents", help="Comma-separated list of agent files") add_parser.add_argument("--commands", help="Comma-separated list of command files") add_parser.add_argument("--skills", help="Comma-separated list of skill directories") add_parser.add_argument("--keywords", help="Comma-separated list of keywords") add_parser.add_argument("--license", default="MIT", help="License type") add_parser.add_argument("--strict", action="store_true", help="Enable strict mode") add_parser.add_argument("--author-name", help="Author name") add_parser.add_argument("--author-url", help="Author URL") add_parser.add_argument( "--marketplace", default=".claude-plugin/marketplace.json", help="Path to marketplace.json", ) # Update command update_parser = subparsers.add_parser("update", help="Update an existing plugin") update_parser.add_argument("--name", required=True, help="Plugin name") update_parser.add_argument("--description", help="Updated description") update_parser.add_argument("--version", help="Updated version") update_parser.add_argument("--category", help="Updated category") update_parser.add_argument("--keywords", help="Updated keywords (comma-separated)") update_parser.add_argument("--add-agent", help="Agent file to add") update_parser.add_argument("--remove-agent", help="Agent file to remove") update_parser.add_argument("--add-command", help="Command file to add") update_parser.add_argument("--remove-command", help="Command file to remove") update_parser.add_argument("--add-skill", help="Skill directory to add") update_parser.add_argument("--remove-skill", help="Skill directory to remove") update_parser.add_argument( "--marketplace", default=".claude-plugin/marketplace.json", help="Path to marketplace.json", ) # Remove command remove_parser = subparsers.add_parser("remove", help="Remove a plugin") remove_parser.add_argument("--name", required=True, help="Plugin name") remove_parser.add_argument( "--marketplace", default=".claude-plugin/marketplace.json", help="Path to marketplace.json", ) # Validate command validate_parser = subparsers.add_parser("validate", help="Validate marketplace.json") validate_parser.add_argument( "--marketplace", default=".claude-plugin/marketplace.json", help="Path to marketplace.json", ) args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) try: updater = MarketplaceUpdater( getattr(args, "marketplace", ".claude-plugin/marketplace.json") ) if args.command == "add": updater.add_plugin( name=args.name, description=args.description, version=args.version, category=args.category, agents=args.agents.split(",") if args.agents else None, commands=args.commands.split(",") if args.commands else None, skills=args.skills.split(",") if args.skills else None, keywords=args.keywords.split(",") if args.keywords else None, license=args.license, strict=args.strict, author_name=args.author_name, author_url=args.author_url, ) elif args.command == "update": updater.update_plugin( name=args.name, description=args.description, version=args.version, category=args.category, keywords=args.keywords.split(",") if args.keywords else None, add_agent=args.add_agent, remove_agent=args.remove_agent, add_command=args.add_command, remove_command=args.remove_command, add_skill=args.add_skill, remove_skill=args.remove_skill, ) elif args.command == "remove": updater.remove_plugin(name=args.name) elif args.command == "validate": if not updater.validate(): sys.exit(1) except Exception as e: print(f"❌ Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()