xsoar-cli 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xsoar-cli might be problematic. Click here for more details.

@@ -0,0 +1,296 @@
1
+ """
2
+ Plugin management commands for XSOAR CLI
3
+
4
+ This module provides CLI commands for managing plugins, including
5
+ listing, loading, reloading, and creating example plugins.
6
+ """
7
+
8
+ import logging
9
+
10
+ import click
11
+
12
+ from .manager import PluginManager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @click.group(help="Manage XSOAR CLI plugins")
18
+ def plugins():
19
+ """Plugin management commands."""
20
+
21
+
22
+ @click.command(help="List all available and loaded plugins")
23
+ @click.option("--verbose", "-v", is_flag=True, help="Show detailed information")
24
+ @click.pass_context
25
+ def list_plugins(ctx: click.Context, verbose: bool):
26
+ """List all plugins in the plugins directory."""
27
+ plugin_manager = PluginManager()
28
+
29
+ # Discover all plugins
30
+ discovered = plugin_manager.discover_plugins()
31
+
32
+ # Load all plugins to get their info
33
+ plugin_manager.load_all_plugins(ignore_errors=True)
34
+
35
+ loaded_info = plugin_manager.get_plugin_info()
36
+ failed_info = plugin_manager.get_failed_plugins()
37
+
38
+ if not discovered:
39
+ click.echo(f"No plugins found in {plugin_manager.plugins_dir}")
40
+ click.echo("Run 'xsoar-cli plugins create-example' to create an example plugin.")
41
+ return
42
+
43
+ click.echo(f"Plugins directory: {plugin_manager.plugins_dir}")
44
+ click.echo(f"Discovered {len(discovered)} plugin files\n")
45
+
46
+ # Show loaded plugins
47
+ if loaded_info:
48
+ click.echo("✅ Loaded Plugins:")
49
+ for plugin_name, info in loaded_info.items():
50
+ if verbose:
51
+ click.echo(f" • {plugin_name}")
52
+ click.echo(f" Name: {info['name']}")
53
+ click.echo(f" Version: {info['version']}")
54
+ click.echo(f" Description: {info['description']}")
55
+ else:
56
+ click.echo(f" • {plugin_name} (v{info['version']})")
57
+ click.echo()
58
+
59
+ # Show failed plugins
60
+ if failed_info:
61
+ click.echo("❌ Failed Plugins:")
62
+ for plugin_name, error in failed_info.items():
63
+ if verbose:
64
+ click.echo(f" • {plugin_name}: {error}")
65
+ else:
66
+ click.echo(f" • {plugin_name}")
67
+ click.echo()
68
+
69
+ # Show unloaded plugins (discovered but not loaded and not failed)
70
+ unloaded = set(discovered) - set(loaded_info.keys()) - set(failed_info.keys())
71
+ if unloaded:
72
+ click.echo("⚠️ Unloaded Plugins:")
73
+ for plugin_name in unloaded:
74
+ click.echo(f" • {plugin_name}")
75
+
76
+ # Show command conflicts
77
+ conflicts = plugin_manager.get_command_conflicts()
78
+ if conflicts:
79
+ click.echo()
80
+ click.echo("⚠️ Command Conflicts:")
81
+ for conflict in conflicts:
82
+ click.echo(f" • Plugin '{conflict['plugin_name']}' command '{conflict['command_name']}' conflicts with core command")
83
+ click.echo(f" Plugin version: {conflict['plugin_version']}")
84
+ click.echo(" Solution: Rename the command in your plugin or use a command group")
85
+
86
+
87
+ @click.command(help="Reload a specific plugin")
88
+ @click.argument("plugin_name", type=str)
89
+ def reload(plugin_name: str):
90
+ """Reload a specific plugin."""
91
+ plugin_manager = PluginManager()
92
+
93
+ try:
94
+ click.echo(f"Reloading plugin: {plugin_name}...")
95
+ plugin = plugin_manager.reload_plugin(plugin_name)
96
+
97
+ if plugin:
98
+ click.echo(f"✅ Successfully reloaded plugin: {plugin_name}")
99
+ click.echo(f" Name: {plugin.name}")
100
+ click.echo(f" Version: {plugin.version}")
101
+ else:
102
+ click.echo(f"❌ Failed to reload plugin: {plugin_name}")
103
+
104
+ except Exception as e:
105
+ click.echo(f"❌ Error reloading plugin {plugin_name}: {e}")
106
+
107
+
108
+ @click.command(help="Create an example plugin file")
109
+ @click.option("--force", is_flag=True, help="Overwrite existing example plugin")
110
+ def create_example(force: bool):
111
+ """Create an example plugin in the plugins directory."""
112
+ plugin_manager = PluginManager()
113
+
114
+ example_file = plugin_manager.plugins_dir / "example_plugin.py"
115
+
116
+ if example_file.exists() and not force:
117
+ click.echo(f"Example plugin already exists at: {example_file}")
118
+ click.echo("Use --force to overwrite it.")
119
+ return
120
+
121
+ plugin_manager.create_example_plugin()
122
+ click.echo(f"✅ Created example plugin at: {example_file}")
123
+ click.echo("\nTo test the example plugin:")
124
+ click.echo(" xsoar-cli example hello --name YourName")
125
+ click.echo(" xsoar-cli example info")
126
+
127
+
128
+ @click.command(help="Show information about a specific plugin")
129
+ @click.argument("plugin_name", type=str)
130
+ def info(plugin_name: str):
131
+ """Show detailed information about a plugin."""
132
+ plugin_manager = PluginManager()
133
+
134
+ try:
135
+ plugin = plugin_manager.load_plugin(plugin_name)
136
+
137
+ if plugin:
138
+ click.echo("Plugin Information:")
139
+ click.echo(f" File: {plugin_manager.plugins_dir / plugin_name}.py")
140
+ click.echo(f" Name: {plugin.name}")
141
+ click.echo(f" Version: {plugin.version}")
142
+ click.echo(f" Description: {plugin.description or 'No description provided'}")
143
+
144
+ # Try to get command info
145
+ try:
146
+ command = plugin.get_command()
147
+ click.echo(f" Command: {command.name}")
148
+ if hasattr(command, "commands"):
149
+ subcommands = list(command.commands.keys())
150
+ if subcommands:
151
+ click.echo(f" Subcommands: {', '.join(subcommands)}")
152
+ except Exception as e:
153
+ click.echo(f" Command: Error loading command ({e})")
154
+ else:
155
+ click.echo(f"❌ Plugin not found or failed to load: {plugin_name}")
156
+
157
+ except Exception as e:
158
+ click.echo(f"❌ Error loading plugin {plugin_name}: {e}")
159
+
160
+
161
+ @click.command(help="Validate all plugins")
162
+ def validate():
163
+ """Validate all plugins in the plugins directory."""
164
+ plugin_manager = PluginManager()
165
+
166
+ discovered = plugin_manager.discover_plugins()
167
+
168
+ if not discovered:
169
+ click.echo(f"No plugins found in {plugin_manager.plugins_dir}")
170
+ return
171
+
172
+ click.echo(f"Validating {len(discovered)} plugins...\n")
173
+
174
+ all_valid = True
175
+
176
+ for plugin_name in discovered:
177
+ try:
178
+ plugin = plugin_manager.load_plugin(plugin_name)
179
+ if plugin:
180
+ # Test that the plugin can provide a command
181
+ command = plugin.get_command()
182
+ if not isinstance(command, (click.Command, click.Group)):
183
+ raise ValueError("get_command() must return a Click Command or Group")
184
+
185
+ click.echo(f"✅ {plugin_name}: Valid")
186
+ else:
187
+ click.echo(f"❌ {plugin_name}: Failed to load")
188
+ all_valid = False
189
+
190
+ except Exception as e:
191
+ click.echo(f"❌ {plugin_name}: {e}")
192
+ all_valid = False
193
+
194
+ # Check for command conflicts by attempting registration
195
+ try:
196
+ from xsoar_cli.cli import cli
197
+
198
+ temp_plugin_manager = PluginManager()
199
+ temp_plugin_manager.load_all_plugins(ignore_errors=True)
200
+ temp_plugin_manager.register_plugin_commands(cli)
201
+
202
+ conflicts = temp_plugin_manager.get_command_conflicts()
203
+ if conflicts:
204
+ click.echo("\n⚠️ Command Conflicts Detected:")
205
+ for conflict in conflicts:
206
+ click.echo(f" • Plugin '{conflict['plugin_name']}' command '{conflict['command_name']}' conflicts with core command")
207
+ click.echo(" Solution: Rename the command or use a command group")
208
+ all_valid = False
209
+ except Exception as e:
210
+ click.echo(f"\n⚠️ Could not check for command conflicts: {e}")
211
+
212
+ click.echo()
213
+ if all_valid:
214
+ click.echo("🎉 All plugins are valid!")
215
+ else:
216
+ click.echo("⚠️ Some plugins have validation errors.")
217
+
218
+
219
+ @click.command(help="Open the plugins directory")
220
+ def open_dir():
221
+ """Open the plugins directory in the system file manager."""
222
+ plugin_manager = PluginManager()
223
+ plugins_dir = plugin_manager.plugins_dir
224
+
225
+ click.echo(f"Plugins directory: {plugins_dir}")
226
+
227
+ # Try to open the directory
228
+ import subprocess
229
+ import sys
230
+
231
+ try:
232
+ if sys.platform == "win32":
233
+ subprocess.run(["explorer", str(plugins_dir)], check=True)
234
+ elif sys.platform == "darwin":
235
+ subprocess.run(["open", str(plugins_dir)], check=True)
236
+ else:
237
+ subprocess.run(["xdg-open", str(plugins_dir)], check=True)
238
+
239
+ click.echo("Opened plugins directory in file manager.")
240
+
241
+ except (subprocess.CalledProcessError, FileNotFoundError):
242
+ click.echo("Could not open directory automatically.")
243
+ click.echo(f"Please navigate to: {plugins_dir}")
244
+
245
+
246
+ # Add all commands to the plugins group
247
+ @click.command(help="Check for command conflicts with core CLI")
248
+ def check_conflicts():
249
+ """Check for command conflicts between plugins and core CLI."""
250
+ plugin_manager = PluginManager()
251
+
252
+ # Load all plugins
253
+ plugin_manager.load_all_plugins(ignore_errors=True)
254
+
255
+ # Check conflicts by attempting registration with a temporary CLI group
256
+ import click
257
+
258
+ temp_cli = click.Group()
259
+
260
+ # Add core commands to temp CLI to simulate real conflicts
261
+ core_commands = ["case", "config", "graph", "manifest", "pack", "playbook", "plugins"]
262
+ for cmd_name in core_commands:
263
+ temp_cli.add_command(click.Command(cmd_name, callback=lambda: None))
264
+
265
+ # Attempt to register plugin commands
266
+ plugin_manager.register_plugin_commands(temp_cli)
267
+
268
+ conflicts = plugin_manager.get_command_conflicts()
269
+
270
+ if not conflicts:
271
+ click.echo("✅ No command conflicts detected!")
272
+ click.echo("All plugin commands have unique names.")
273
+ return
274
+
275
+ click.echo(f"⚠️ Found {len(conflicts)} command conflict(s):")
276
+ click.echo()
277
+
278
+ for conflict in conflicts:
279
+ click.echo(f"🔸 Plugin: {conflict['plugin_name']} (v{conflict['plugin_version']})")
280
+ click.echo(f" Command: '{conflict['command_name']}'")
281
+ click.echo(" Conflicts with: Core CLI command")
282
+ click.echo()
283
+
284
+ click.echo("💡 Solutions:")
285
+ click.echo(" • Rename the conflicting command in your plugin")
286
+ click.echo(" • Use a command group to namespace your commands")
287
+ click.echo(" • Example: Instead of 'case', use 'mycase' or 'myplugin case'")
288
+
289
+
290
+ plugins.add_command(list_plugins, name="list")
291
+ plugins.add_command(reload)
292
+ plugins.add_command(create_example, name="create-example")
293
+ plugins.add_command(info)
294
+ plugins.add_command(validate)
295
+ plugins.add_command(check_conflicts, name="check-conflicts")
296
+ plugins.add_command(open_dir, name="open")
@@ -0,0 +1,399 @@
1
+ """
2
+ Plugin Manager for XSOAR CLI
3
+
4
+ This module handles the discovery, loading, and management of plugins
5
+ for the xsoar-cli application from a local directory.
6
+ """
7
+
8
+ import importlib.util
9
+ import logging
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional, Type
13
+
14
+ import click
15
+
16
+ from . import PluginLoadError, PluginRegistrationError, XSOARPlugin
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class PluginManager:
22
+ """
23
+ Manages the discovery, loading, and registration of XSOAR CLI plugins
24
+ from the ~/.local/xsoar-cli/plugins directory.
25
+ """
26
+
27
+ def __init__(self, plugins_dir: Optional[Path] = None):
28
+ """
29
+ Initialize the plugin manager.
30
+
31
+ Args:
32
+ plugins_dir: Custom plugins directory. If None, uses ~/.local/xsoar-cli/plugins
33
+ """
34
+ if plugins_dir is None:
35
+ self.plugins_dir = Path.home() / ".local" / "xsoar-cli" / "plugins"
36
+ else:
37
+ self.plugins_dir = plugins_dir
38
+
39
+ self.loaded_plugins: Dict[str, XSOARPlugin] = {}
40
+ self.failed_plugins: Dict[str, Exception] = {}
41
+ self.command_conflicts: List[Dict[str, str]] = []
42
+
43
+ # Ensure plugins directory exists
44
+ self.plugins_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ # Add plugins directory to Python path if not already there
47
+ plugins_dir_str = str(self.plugins_dir)
48
+ if plugins_dir_str not in sys.path:
49
+ sys.path.insert(0, plugins_dir_str)
50
+
51
+ def discover_plugins(self) -> List[str]:
52
+ """
53
+ Discover available plugins by scanning the plugins directory for Python files.
54
+
55
+ Returns:
56
+ List of plugin module names found
57
+ """
58
+ plugin_names = []
59
+
60
+ if not self.plugins_dir.exists():
61
+ logger.info(f"Plugins directory does not exist: {self.plugins_dir}")
62
+ return plugin_names
63
+
64
+ for file_path in self.plugins_dir.glob("*.py"):
65
+ if file_path.name.startswith("__"):
66
+ continue # Skip __init__.py, __pycache__, etc.
67
+
68
+ module_name = file_path.stem
69
+ plugin_names.append(module_name)
70
+
71
+ logger.info(f"Discovered {len(plugin_names)} plugin files: {plugin_names}")
72
+ return plugin_names
73
+
74
+ def _load_module_from_file(self, module_name: str, file_path: Path):
75
+ """
76
+ Load a Python module from a file path.
77
+
78
+ Args:
79
+ module_name: Name to give the module
80
+ file_path: Path to the Python file
81
+
82
+ Returns:
83
+ The loaded module
84
+ """
85
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
86
+ if spec is None or spec.loader is None:
87
+ raise PluginLoadError(f"Could not load module spec for {file_path}")
88
+
89
+ module = importlib.util.module_from_spec(spec)
90
+
91
+ # Inject XSOARPlugin class into the module's namespace
92
+ # This allows plugins to use XSOARPlugin without complex imports
93
+ from . import XSOARPlugin
94
+
95
+ module.XSOARPlugin = XSOARPlugin
96
+
97
+ sys.modules[module_name] = module
98
+ spec.loader.exec_module(module)
99
+ return module
100
+
101
+ def _find_plugin_classes(self, module) -> List[Type[XSOARPlugin]]:
102
+ """
103
+ Find all XSOARPlugin classes in a module.
104
+
105
+ Args:
106
+ module: The module to search
107
+
108
+ Returns:
109
+ List of plugin classes found
110
+ """
111
+ plugin_classes = []
112
+
113
+ for attr_name in dir(module):
114
+ attr = getattr(module, attr_name)
115
+
116
+ # Check if it's a class that inherits from XSOARPlugin
117
+ if isinstance(attr, type) and issubclass(attr, XSOARPlugin) and attr is not XSOARPlugin:
118
+ plugin_classes.append(attr)
119
+
120
+ return plugin_classes
121
+
122
+ def load_plugin(self, plugin_name: str) -> Optional[XSOARPlugin]:
123
+ """
124
+ Load a single plugin by name.
125
+
126
+ Args:
127
+ plugin_name: Name of the plugin module to load
128
+
129
+ Returns:
130
+ The loaded plugin instance, or None if loading failed
131
+
132
+ Raises:
133
+ PluginLoadError: If the plugin fails to load
134
+ """
135
+ if plugin_name in self.loaded_plugins:
136
+ return self.loaded_plugins[plugin_name]
137
+
138
+ try:
139
+ file_path = self.plugins_dir / f"{plugin_name}.py"
140
+ if not file_path.exists():
141
+ raise PluginLoadError(f"Plugin file not found: {file_path}")
142
+
143
+ # Load the module
144
+ module = self._load_module_from_file(plugin_name, file_path)
145
+
146
+ # Find plugin classes in the module
147
+ plugin_classes = self._find_plugin_classes(module)
148
+
149
+ if not plugin_classes:
150
+ raise PluginLoadError(
151
+ f"No XSOARPlugin classes found in {plugin_name}.py",
152
+ )
153
+
154
+ if len(plugin_classes) > 1:
155
+ logger.warning(
156
+ f"Multiple plugin classes found in {plugin_name}.py, using the first one",
157
+ )
158
+
159
+ # Instantiate the first plugin class found
160
+ plugin_class = plugin_classes[0]
161
+ plugin_instance = plugin_class()
162
+
163
+ # Initialize the plugin
164
+ try:
165
+ plugin_instance.initialize()
166
+ except Exception as e:
167
+ raise PluginLoadError(f"Plugin '{plugin_name}' initialization failed: {e}")
168
+
169
+ # Store the loaded plugin
170
+ self.loaded_plugins[plugin_name] = plugin_instance
171
+ logger.info(f"Successfully loaded plugin: {plugin_name}")
172
+
173
+ return plugin_instance
174
+
175
+ except Exception as e:
176
+ self.failed_plugins[plugin_name] = e
177
+ logger.error(f"Failed to load plugin '{plugin_name}': {e}")
178
+ raise PluginLoadError(f"Failed to load plugin '{plugin_name}': {e}")
179
+
180
+ def load_all_plugins(self, ignore_errors: bool = True) -> Dict[str, XSOARPlugin]:
181
+ """
182
+ Load all discovered plugins.
183
+
184
+ Args:
185
+ ignore_errors: If True, continue loading other plugins when one fails
186
+
187
+ Returns:
188
+ Dictionary of successfully loaded plugins
189
+
190
+ Raises:
191
+ PluginLoadError: If ignore_errors is False and any plugin fails to load
192
+ """
193
+ discovered_plugins = self.discover_plugins()
194
+
195
+ for plugin_name in discovered_plugins:
196
+ try:
197
+ self.load_plugin(plugin_name)
198
+ except PluginLoadError as e:
199
+ if not ignore_errors:
200
+ raise
201
+ logger.warning(f"Skipping failed plugin: {e}")
202
+
203
+ return self.loaded_plugins.copy()
204
+
205
+ def register_plugin_commands(self, cli_group: click.Group) -> None:
206
+ """
207
+ Register all loaded plugin commands with the CLI group.
208
+
209
+ Args:
210
+ cli_group: The Click group to register commands with
211
+
212
+ Raises:
213
+ PluginRegistrationError: If a plugin command fails to register
214
+ """
215
+ conflicts = []
216
+
217
+ for plugin_name, plugin in self.loaded_plugins.items():
218
+ try:
219
+ command = plugin.get_command()
220
+ if not isinstance(command, (click.Command, click.Group)):
221
+ raise PluginRegistrationError(
222
+ f"Plugin '{plugin_name}' get_command() must return a Click Command or Group",
223
+ )
224
+
225
+ # Ensure the command name doesn't conflict with existing commands
226
+ if command.name in cli_group.commands:
227
+ conflict_info = {
228
+ "plugin_name": plugin_name,
229
+ "command_name": command.name,
230
+ "plugin_version": plugin.version,
231
+ }
232
+ conflicts.append(conflict_info)
233
+ logger.warning(
234
+ f"Plugin '{plugin_name}' command '{command.name}' conflicts with existing command",
235
+ )
236
+ continue
237
+
238
+ cli_group.add_command(command)
239
+ logger.info(f"Registered command '{command.name}' from plugin '{plugin_name}'")
240
+
241
+ except Exception as e:
242
+ error_msg = f"Failed to register plugin '{plugin_name}': {e}"
243
+ logger.error(error_msg)
244
+ raise PluginRegistrationError(error_msg)
245
+
246
+ # Store conflicts for later reporting
247
+ self.command_conflicts = conflicts
248
+
249
+ def unload_plugin(self, plugin_name: str) -> None:
250
+ """
251
+ Unload a plugin and call its cleanup method.
252
+
253
+ Args:
254
+ plugin_name: Name of the plugin to unload
255
+ """
256
+ if plugin_name in self.loaded_plugins:
257
+ plugin = self.loaded_plugins[plugin_name]
258
+ try:
259
+ plugin.cleanup()
260
+ except Exception as e:
261
+ logger.warning(f"Plugin '{plugin_name}' cleanup failed: {e}")
262
+ del self.loaded_plugins[plugin_name]
263
+ logger.info(f"Unloaded plugin: {plugin_name}")
264
+
265
+ def unload_all_plugins(self) -> None:
266
+ """Unload all plugins and call their cleanup methods."""
267
+ plugin_names = list(self.loaded_plugins.keys())
268
+ for plugin_name in plugin_names:
269
+ self.unload_plugin(plugin_name)
270
+
271
+ def get_plugin_info(self) -> Dict[str, Dict[str, str]]:
272
+ """
273
+ Get information about all loaded plugins.
274
+
275
+ Returns:
276
+ Dictionary with plugin information
277
+ """
278
+ info = {}
279
+ for plugin_name, plugin in self.loaded_plugins.items():
280
+ info[plugin_name] = {
281
+ "name": plugin.name,
282
+ "version": plugin.version,
283
+ "description": plugin.description or "No description provided",
284
+ }
285
+ return info
286
+
287
+ def get_failed_plugins(self) -> Dict[str, str]:
288
+ """
289
+ Get information about plugins that failed to load.
290
+
291
+ Returns:
292
+ Dictionary with plugin names and error messages
293
+ """
294
+ return {name: str(error) for name, error in self.failed_plugins.items()}
295
+
296
+ def get_command_conflicts(self) -> List[Dict[str, str]]:
297
+ """
298
+ Get information about command conflicts.
299
+
300
+ Returns:
301
+ List of dictionaries with conflict information
302
+ """
303
+ return self.command_conflicts.copy()
304
+
305
+ def reload_plugin(self, plugin_name: str) -> Optional[XSOARPlugin]:
306
+ """
307
+ Reload a plugin by unloading and loading it again.
308
+
309
+ Args:
310
+ plugin_name: Name of the plugin to reload
311
+
312
+ Returns:
313
+ The reloaded plugin instance, or None if reloading failed
314
+ """
315
+ # Unload if already loaded
316
+ if plugin_name in self.loaded_plugins:
317
+ self.unload_plugin(plugin_name)
318
+
319
+ # Remove from failed plugins if it was there
320
+ if plugin_name in self.failed_plugins:
321
+ del self.failed_plugins[plugin_name]
322
+
323
+ # Remove from sys.modules to force reload
324
+ if plugin_name in sys.modules:
325
+ del sys.modules[plugin_name]
326
+
327
+ # Load again
328
+ return self.load_plugin(plugin_name)
329
+
330
+ def create_example_plugin(self) -> None:
331
+ """
332
+ Create an example plugin file in the plugins directory.
333
+ """
334
+ example_content = '''"""
335
+ Example XSOAR CLI Plugin
336
+
337
+ This is an example plugin that demonstrates how to create custom commands
338
+ for the xsoar-cli application.
339
+
340
+ The XSOARPlugin class is automatically injected into the plugin namespace,
341
+ so you don't need to import it.
342
+ """
343
+
344
+ import click
345
+
346
+
347
+ class ExamplePlugin(XSOARPlugin):
348
+ """Example plugin implementation."""
349
+
350
+ @property
351
+ def name(self) -> str:
352
+ return "example"
353
+
354
+ @property
355
+ def version(self) -> str:
356
+ return "1.0.0"
357
+
358
+ @property
359
+ def description(self) -> str:
360
+ return "An example plugin demonstrating the plugin system"
361
+
362
+ def get_command(self) -> click.Command:
363
+ """Return the Click command for this plugin."""
364
+
365
+ @click.group(help="Example plugin commands")
366
+ def example():
367
+ pass
368
+
369
+ @click.command(help="Say hello")
370
+ @click.option("--name", default="World", help="Name to greet")
371
+ def hello(name: str):
372
+ click.echo(f"Hello, {name}! This is from the example plugin.")
373
+
374
+ @click.command(help="Show plugin info")
375
+ def info():
376
+ click.echo(f"Plugin: {self.name}")
377
+ click.echo(f"Version: {self.version}")
378
+ click.echo(f"Description: {self.description}")
379
+
380
+ example.add_command(hello)
381
+ example.add_command(info)
382
+
383
+ return example
384
+
385
+ def initialize(self):
386
+ """Initialize the plugin."""
387
+ click.echo("Example plugin initialized!")
388
+
389
+ def cleanup(self):
390
+ """Cleanup plugin resources."""
391
+ pass
392
+ '''
393
+
394
+ example_file = self.plugins_dir / "example_plugin.py"
395
+ if not example_file.exists():
396
+ example_file.write_text(example_content)
397
+ logger.info(f"Created example plugin at: {example_file}")
398
+ else:
399
+ logger.info(f"Example plugin already exists at: {example_file}")