"""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"/{resolved.parent.name}/{resolved.name}"