feat: implement comprehensive plugin architecture and extensions system (issue #19)
Complete plugin system implementation providing extensible architecture for MarkiTect: 🏗️ **Core Plugin Architecture**: - BasePlugin abstract class with lifecycle management (initialize/cleanup) - Specialized plugin types: ProcessorPlugin, FormatterPlugin, ValidatorPlugin, ExporterPlugin, CommandPlugin - PluginMetadata system with version, dependencies, and type information - Plugin initialization and configuration validation 🔍 **Plugin Discovery & Management**: - PluginManager with automatic discovery from built-in modules and directories - PluginRegistry for centralized plugin registration and lifecycle management - Support for plugin loading, unloading, and reloading with configuration - Plugin discovery from multiple sources (built-in, directories, packages) 🛠️ **CLI Integration**: - markitect plugin-list: List all available plugins with metadata - markitect plugin-load: Load plugins with optional configuration - markitect plugin-unload: Unload plugins and cleanup resources - markitect plugin-info: Show detailed plugin information - markitect plugin-discover: Discover and refresh plugin catalog 📦 **Built-in Plugins**: - JSON/YAML/Table formatters for output formatting - Markdown/Text processors for content processing - Auto-registered via @register_plugin decorator - Comprehensive configuration options 🔧 **Developer Experience**: - @register_plugin decorator for easy plugin registration - Plugin configuration validation and error handling - Comprehensive API documentation with examples - Plugin development guide and best practices 📋 **Example Plugins**: - Advanced text processor with case conversion and pattern replacement - XML/CSV formatters demonstrating custom output formats - Complete examples showing plugin development patterns 🧪 **Test Coverage**: - 59 comprehensive tests covering all plugin functionality - Tests for plugin lifecycle, registration, discovery, and CLI integration - Error handling and edge case coverage - Built-in plugin validation Technical Implementation: - Plugin types: processor, formatter, validator, exporter, generator, importer, transformer, extension, backend, command - Configuration-driven plugin management with YAML/JSON support - Graceful error handling and plugin isolation - Plugin dependency validation and compatibility checking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
240
markitect/cli.py
240
markitect/cli.py
@@ -5040,6 +5040,246 @@ def config_help(config, key):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Import PluginType for the CLI commands
|
||||
from .plugins.base import PluginType
|
||||
|
||||
# Plugin Management Commands
|
||||
|
||||
@cli.command(name='plugin-list')
|
||||
@click.option('--type', 'plugin_type', type=click.Choice([pt.value for pt in PluginType]),
|
||||
help='Filter by plugin type')
|
||||
@click.option('--format', 'output_format', type=click.Choice(['table', 'json', 'yaml']),
|
||||
default='table', help='Output format')
|
||||
@pass_config
|
||||
def plugin_list(config, plugin_type, output_format):
|
||||
"""List all available plugins.
|
||||
|
||||
Shows discovered and loaded plugins with their metadata and status.
|
||||
|
||||
Examples:
|
||||
markitect plugin-list
|
||||
markitect plugin-list --type processor
|
||||
markitect plugin-list --format json
|
||||
"""
|
||||
try:
|
||||
from .plugins import PluginManager, PluginType
|
||||
|
||||
manager = PluginManager()
|
||||
manager.discover_plugins()
|
||||
|
||||
# Filter by type if specified
|
||||
filter_type = None
|
||||
if plugin_type:
|
||||
filter_type = PluginType(plugin_type)
|
||||
|
||||
plugins = manager.list_plugins(filter_type)
|
||||
|
||||
if output_format == 'table':
|
||||
if not plugins:
|
||||
click.echo("No plugins found.")
|
||||
return
|
||||
|
||||
# Create table output
|
||||
click.echo("📦 Available Plugins:")
|
||||
click.echo()
|
||||
|
||||
for name, info in plugins.items():
|
||||
status = "✅ Loaded" if info.get('loaded', False) else "⚪ Available"
|
||||
click.echo(f"{status} {name}")
|
||||
click.echo(f" Type: {info.get('type', 'unknown')}")
|
||||
click.echo(f" Version: {info.get('version', 'unknown')}")
|
||||
click.echo(f" Description: {info.get('description', 'No description')}")
|
||||
if info.get('author'):
|
||||
click.echo(f" Author: {info['author']}")
|
||||
click.echo()
|
||||
|
||||
else:
|
||||
click.echo(format_output(plugins, output_format))
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to list plugins: {e}", err=True)
|
||||
if config.get('verbose'):
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command(name='plugin-load')
|
||||
@click.argument('plugin_name', type=str)
|
||||
@click.option('--config-data', type=str, help='JSON configuration data for plugin')
|
||||
@pass_config
|
||||
def plugin_load(config, plugin_name, config_data):
|
||||
"""Load a specific plugin.
|
||||
|
||||
Load and initialize a plugin with optional configuration.
|
||||
|
||||
Examples:
|
||||
markitect plugin-load json_formatter
|
||||
markitect plugin-load my_processor --config-data '{"param": "value"}'
|
||||
"""
|
||||
try:
|
||||
from .plugins import PluginManager
|
||||
import json
|
||||
|
||||
manager = PluginManager()
|
||||
|
||||
# Parse config data if provided
|
||||
plugin_config = {}
|
||||
if config_data:
|
||||
try:
|
||||
plugin_config = json.loads(config_data)
|
||||
except json.JSONDecodeError as e:
|
||||
click.echo(f"❌ Invalid JSON in config-data: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if config.get('verbose'):
|
||||
click.echo(f"🔍 Attempting to load plugin '{plugin_name}'...")
|
||||
|
||||
plugin = manager.load_plugin(plugin_name, plugin_config)
|
||||
|
||||
if plugin:
|
||||
click.echo(f"✅ Plugin '{plugin_name}' loaded successfully")
|
||||
click.echo(f" Type: {plugin.metadata.plugin_type.value}")
|
||||
click.echo(f" Version: {plugin.metadata.version}")
|
||||
else:
|
||||
click.echo(f"❌ Failed to load plugin '{plugin_name}'", err=True)
|
||||
if config.get('verbose'):
|
||||
# Additional debug info
|
||||
discovered = manager.discover_plugins()
|
||||
if plugin_name in discovered:
|
||||
click.echo(f" Plugin found in discovery: {discovered[plugin_name]}")
|
||||
else:
|
||||
click.echo(f" Plugin not found in discovery. Available: {list(discovered.keys())[:5]}")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to load plugin: {e}", err=True)
|
||||
if config.get('verbose'):
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command(name='plugin-unload')
|
||||
@click.argument('plugin_name', type=str)
|
||||
@pass_config
|
||||
def plugin_unload(config, plugin_name):
|
||||
"""Unload a plugin.
|
||||
|
||||
Unload and cleanup a previously loaded plugin.
|
||||
|
||||
Examples:
|
||||
markitect plugin-unload json_formatter
|
||||
"""
|
||||
try:
|
||||
from .plugins import PluginManager
|
||||
|
||||
manager = PluginManager()
|
||||
|
||||
if manager.unload_plugin(plugin_name):
|
||||
click.echo(f"✅ Plugin '{plugin_name}' unloaded successfully")
|
||||
else:
|
||||
click.echo(f"❌ Plugin '{plugin_name}' not found or not loaded", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to unload plugin: {e}", err=True)
|
||||
if config.get('verbose'):
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command(name='plugin-info')
|
||||
@click.argument('plugin_name', type=str)
|
||||
@click.option('--format', 'output_format', type=click.Choice(['simple', 'json', 'yaml']),
|
||||
default='simple', help='Output format')
|
||||
@pass_config
|
||||
def plugin_info(config, plugin_name, output_format):
|
||||
"""Show detailed information about a plugin.
|
||||
|
||||
Display comprehensive information about a specific plugin including
|
||||
metadata, configuration, and status.
|
||||
|
||||
Examples:
|
||||
markitect plugin-info json_formatter
|
||||
markitect plugin-info my_processor --format json
|
||||
"""
|
||||
try:
|
||||
from .plugins import PluginManager
|
||||
|
||||
manager = PluginManager()
|
||||
manager.discover_plugins()
|
||||
|
||||
plugins = manager.list_plugins()
|
||||
|
||||
if plugin_name not in plugins:
|
||||
click.echo(f"❌ Plugin '{plugin_name}' not found", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
plugin_info = plugins[plugin_name]
|
||||
|
||||
if output_format == 'simple':
|
||||
click.echo(f"📦 Plugin: {plugin_name}")
|
||||
click.echo(f" Name: {plugin_info.get('name', 'N/A')}")
|
||||
click.echo(f" Version: {plugin_info.get('version', 'N/A')}")
|
||||
click.echo(f" Type: {plugin_info.get('type', 'N/A')}")
|
||||
click.echo(f" Description: {plugin_info.get('description', 'N/A')}")
|
||||
click.echo(f" Author: {plugin_info.get('author', 'N/A')}")
|
||||
click.echo(f" Status: {'Loaded' if plugin_info.get('loaded', False) else 'Available'}")
|
||||
|
||||
if plugin_info.get('dependencies'):
|
||||
click.echo(f" Dependencies: {', '.join(plugin_info['dependencies'])}")
|
||||
|
||||
if plugin_info.get('markitect_version'):
|
||||
click.echo(f" MarkiTect Version: {plugin_info['markitect_version']}")
|
||||
else:
|
||||
click.echo(format_output(plugin_info, output_format))
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to get plugin info: {e}", err=True)
|
||||
if config.get('verbose'):
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command(name='plugin-discover')
|
||||
@click.option('--refresh', is_flag=True, help='Force refresh of plugin discovery')
|
||||
@pass_config
|
||||
def plugin_discover(config, refresh):
|
||||
"""Discover available plugins.
|
||||
|
||||
Scan for plugins in configured directories and report findings.
|
||||
|
||||
Examples:
|
||||
markitect plugin-discover
|
||||
markitect plugin-discover --refresh
|
||||
"""
|
||||
try:
|
||||
from .plugins import PluginManager
|
||||
|
||||
manager = PluginManager()
|
||||
discovered = manager.discover_plugins(refresh=refresh)
|
||||
|
||||
click.echo(f"🔍 Plugin Discovery Complete")
|
||||
click.echo(f" Found {len(discovered)} plugins")
|
||||
|
||||
if discovered:
|
||||
click.echo(" Discovered plugins:")
|
||||
for name in sorted(discovered.keys()):
|
||||
click.echo(f" • {name}")
|
||||
else:
|
||||
click.echo(" No plugins found")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to discover plugins: {e}", err=True)
|
||||
if config.get('verbose'):
|
||||
import traceback
|
||||
click.echo(traceback.format_exc(), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Register issue management commands
|
||||
cli.add_command(issues_group)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user