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:
2025-10-03 11:23:32 +02:00
parent e6adb3e6db
commit b0de32d083
13 changed files with 3160 additions and 0 deletions

View File

@@ -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)