75 lines
2.5 KiB
Python
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}"
|