BEFORE: /opt/aitbc/cli/ ├── aitbc_cli/ # Python package (box in a box) │ ├── commands/ │ ├── main.py │ └── ... ├── setup.py AFTER: /opt/aitbc/cli/ # Flat structure ├── commands/ # Direct access ├── main.py # Direct access ├── auth/ ├── config/ ├── core/ ├── models/ ├── utils/ ├── plugins.py └── setup.py CHANGES MADE: - Moved all files from aitbc_cli/ to cli/ root - Fixed all relative imports (from . to absolute imports) - Updated setup.py entry point: aitbc_cli.main → main - Added CLI directory to Python path in entry script - Simplified deployment.py to remove dependency on deleted core.deployment - Fixed import paths in all command files - Recreated virtual environment with new structure BENEFITS: - Eliminated 'box in a box' nesting - Simpler directory structure - Direct access to all modules - Cleaner imports - Easier maintenance and development - CLI works with both 'python main.py' and 'aitbc' commands
187 lines
5.2 KiB
Python
Executable File
187 lines
5.2 KiB
Python
Executable File
"""Plugin system for AITBC CLI custom commands"""
|
|
|
|
import importlib
|
|
import importlib.util
|
|
import json
|
|
import click
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
PLUGIN_DIR = Path.home() / ".aitbc" / "plugins"
|
|
|
|
|
|
def get_plugin_dir() -> Path:
|
|
"""Get and ensure plugin directory exists"""
|
|
PLUGIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
return PLUGIN_DIR
|
|
|
|
|
|
def load_plugins(cli_group):
|
|
"""Load all plugins and register them with the CLI group"""
|
|
plugin_dir = get_plugin_dir()
|
|
manifest_file = plugin_dir / "plugins.json"
|
|
|
|
if not manifest_file.exists():
|
|
return
|
|
|
|
with open(manifest_file) as f:
|
|
manifest = json.load(f)
|
|
|
|
for plugin_info in manifest.get("plugins", []):
|
|
if not plugin_info.get("enabled", True):
|
|
continue
|
|
|
|
plugin_path = plugin_dir / plugin_info["file"]
|
|
if not plugin_path.exists():
|
|
continue
|
|
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(
|
|
plugin_info["name"], str(plugin_path)
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
# Look for a click group or command named 'plugin_command'
|
|
if hasattr(module, "plugin_command"):
|
|
cli_group.add_command(module.plugin_command)
|
|
except Exception:
|
|
pass # Skip broken plugins silently
|
|
|
|
|
|
@click.group()
|
|
def plugin():
|
|
"""Manage CLI plugins"""
|
|
pass
|
|
|
|
|
|
@plugin.command(name="list")
|
|
@click.pass_context
|
|
def list_plugins(ctx):
|
|
"""List installed plugins"""
|
|
from .utils import output
|
|
|
|
plugin_dir = get_plugin_dir()
|
|
manifest_file = plugin_dir / "plugins.json"
|
|
|
|
if not manifest_file.exists():
|
|
output({"message": "No plugins installed"}, ctx.obj.get('output_format', 'table'))
|
|
return
|
|
|
|
with open(manifest_file) as f:
|
|
manifest = json.load(f)
|
|
|
|
plugins = manifest.get("plugins", [])
|
|
if not plugins:
|
|
output({"message": "No plugins installed"}, ctx.obj.get('output_format', 'table'))
|
|
else:
|
|
output(plugins, ctx.obj.get('output_format', 'table'))
|
|
|
|
|
|
@plugin.command()
|
|
@click.argument("name")
|
|
@click.argument("file_path", type=click.Path(exists=True))
|
|
@click.option("--description", default="", help="Plugin description")
|
|
@click.pass_context
|
|
def install(ctx, name: str, file_path: str, description: str):
|
|
"""Install a plugin from a Python file"""
|
|
import shutil
|
|
from .utils import output, error, success
|
|
|
|
plugin_dir = get_plugin_dir()
|
|
manifest_file = plugin_dir / "plugins.json"
|
|
|
|
# Copy plugin file
|
|
dest = plugin_dir / f"{name}.py"
|
|
shutil.copy2(file_path, dest)
|
|
|
|
# Update manifest
|
|
manifest = {"plugins": []}
|
|
if manifest_file.exists():
|
|
with open(manifest_file) as f:
|
|
manifest = json.load(f)
|
|
|
|
# Remove existing entry with same name
|
|
manifest["plugins"] = [p for p in manifest["plugins"] if p["name"] != name]
|
|
manifest["plugins"].append({
|
|
"name": name,
|
|
"file": f"{name}.py",
|
|
"description": description,
|
|
"enabled": True
|
|
})
|
|
|
|
with open(manifest_file, "w") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
|
|
success(f"Plugin '{name}' installed")
|
|
output({"name": name, "file": str(dest), "status": "installed"}, ctx.obj.get('output_format', 'table'))
|
|
|
|
|
|
@plugin.command()
|
|
@click.argument("name")
|
|
@click.pass_context
|
|
def uninstall(ctx, name: str):
|
|
"""Uninstall a plugin"""
|
|
from .utils import output, error, success
|
|
|
|
plugin_dir = get_plugin_dir()
|
|
manifest_file = plugin_dir / "plugins.json"
|
|
|
|
if not manifest_file.exists():
|
|
error(f"Plugin '{name}' not found")
|
|
return
|
|
|
|
with open(manifest_file) as f:
|
|
manifest = json.load(f)
|
|
|
|
plugin_entry = next((p for p in manifest["plugins"] if p["name"] == name), None)
|
|
if not plugin_entry:
|
|
error(f"Plugin '{name}' not found")
|
|
return
|
|
|
|
# Remove file
|
|
plugin_file = plugin_dir / plugin_entry["file"]
|
|
if plugin_file.exists():
|
|
plugin_file.unlink()
|
|
|
|
# Update manifest
|
|
manifest["plugins"] = [p for p in manifest["plugins"] if p["name"] != name]
|
|
with open(manifest_file, "w") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
|
|
success(f"Plugin '{name}' uninstalled")
|
|
output({"name": name, "status": "uninstalled"}, ctx.obj.get('output_format', 'table'))
|
|
|
|
|
|
@plugin.command()
|
|
@click.argument("name")
|
|
@click.argument("state", type=click.Choice(["enable", "disable"]))
|
|
@click.pass_context
|
|
def toggle(ctx, name: str, state: str):
|
|
"""Enable or disable a plugin"""
|
|
from .utils import output, error, success
|
|
|
|
plugin_dir = get_plugin_dir()
|
|
manifest_file = plugin_dir / "plugins.json"
|
|
|
|
if not manifest_file.exists():
|
|
error(f"Plugin '{name}' not found")
|
|
return
|
|
|
|
with open(manifest_file) as f:
|
|
manifest = json.load(f)
|
|
|
|
plugin_entry = next((p for p in manifest["plugins"] if p["name"] == name), None)
|
|
if not plugin_entry:
|
|
error(f"Plugin '{name}' not found")
|
|
return
|
|
|
|
plugin_entry["enabled"] = (state == "enable")
|
|
|
|
with open(manifest_file, "w") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
|
|
success(f"Plugin '{name}' {'enabled' if state == 'enable' else 'disabled'}")
|
|
output({"name": name, "enabled": plugin_entry["enabled"]}, ctx.obj.get('output_format', 'table'))
|