Files
beaver_project/app-instance/backend/beaver/plugins/discovery.py

75 lines
2.5 KiB
Python

"""Plugin package discovery."""
from __future__ import annotations
from pathlib import Path
from typing import Iterable
from .manifest import load_plugin_manifest
from .models import PluginDiscoveryError, PluginDiscoveryResult, PluginManifest
def discover_plugins(
workspace: str | Path,
*,
search_paths: Iterable[str | Path] = (),
) -> PluginDiscoveryResult:
workspace_root = Path(workspace).resolve()
candidates: list[Path] = []
candidates.extend(_candidate_manifest_paths(workspace_root / "plugins"))
for root in search_paths:
candidates.extend(_candidate_manifest_paths(Path(root).expanduser()))
manifests_by_id: dict[str, list[PluginManifest]] = {}
errors: list[PluginDiscoveryError] = []
for manifest_path in candidates:
try:
manifest = load_plugin_manifest(manifest_path, workspace=workspace_root)
except Exception as exc: # noqa: BLE001 - discovery reports per-path errors.
errors.append(
PluginDiscoveryError(
path=manifest_path,
display_path=_display_path(manifest_path, workspace_root),
message=str(exc),
plugin_id=None,
)
)
continue
manifests_by_id.setdefault(manifest.plugin_id, []).append(manifest)
manifests: dict[str, PluginManifest] = {}
for plugin_id, matches in manifests_by_id.items():
if len(matches) == 1:
manifests[plugin_id] = matches[0]
continue
for manifest in matches:
errors.append(
PluginDiscoveryError(
path=manifest.manifest_path,
display_path=manifest.display_path,
message=f"Duplicate plugin id: {plugin_id}",
plugin_id=plugin_id,
)
)
return PluginDiscoveryResult(manifests=manifests, errors=errors)
def _candidate_manifest_paths(root: Path) -> list[Path]:
if not root.exists() or not root.is_dir():
return []
results: list[Path] = []
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
manifest = child / "beaver.plugin.json"
if manifest.is_file():
results.append(manifest)
return results
def _display_path(path: Path, workspace: Path) -> str:
resolved = path.resolve()
if resolved.is_relative_to(workspace):
return resolved.relative_to(workspace).as_posix()
return f"<external>/{resolved.parent.name}/{resolved.name}"