Refactor plugin registry into separate files per plugin

This commit is contained in:
pleb
2026-07-05 14:24:58 -07:00
parent 1b4087c5b5
commit 1858f76766
62 changed files with 730 additions and 1008 deletions
+6 -6
View File
@@ -106,7 +106,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
installed.add_argument("--instance", required=True)
installed.add_argument("--registry", default="registry/plugins.toml")
installed.add_argument("--registry", default="registry/plugins")
installed.add_argument("--lockfile")
installed.add_argument("--json", action="store_true", help="Print full JSON output")
@@ -116,7 +116,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
check.add_argument("--instance", required=True)
check.add_argument("--registry", default="registry/plugins.toml")
check.add_argument("--registry", default="registry/plugins")
check.add_argument("--lockfile")
check.add_argument("--json", action="store_true", help="Print full JSON check output")
@@ -126,7 +126,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
bootstrap.add_argument("--instance", required=True)
bootstrap.add_argument("--registry", default="registry/plugins.toml")
bootstrap.add_argument("--registry", default="registry/plugins")
bootstrap.add_argument("--lockfile")
bootstrap.add_argument("--proton", help="Path to Proton executable (Linux/Proton installs)")
bootstrap.add_argument("--native", action="store_true", help="Run IPA.exe -n natively instead of through Proton")
@@ -146,7 +146,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
updates.add_argument("--instance", required=True)
updates.add_argument("--registry", default="registry/plugins.toml")
updates.add_argument("--registry", default="registry/plugins")
updates.add_argument("--lockfile")
updates.add_argument("--plugin", action="append", help="Check only this locked plugin id; repeatable")
updates.add_argument("--include-prerelease", action="store_true", help="Include prerelease GitHub releases")
@@ -158,7 +158,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
plan.add_argument("--instance", required=True)
plan.add_argument("--registry", default="registry/plugins.toml")
plan.add_argument("--registry", default="registry/plugins")
plan.add_argument("--lockfile")
plan.add_argument("--plugin", "--update", action="append", help="Plan only this locked plugin id; repeatable")
@@ -193,7 +193,7 @@ def build_parser() -> argparse.ArgumentParser:
parents=[_common_parent()],
)
enable.add_argument("--instance", required=True)
enable.add_argument("--registry", default="registry/plugins.toml")
enable.add_argument("--registry", default="registry/plugins")
enable.add_argument("--lockfile")
enable.add_argument("plugin")
+40 -22
View File
@@ -58,33 +58,51 @@ def _load_toml(path: Path) -> dict[str, Any]:
return tomllib.load(handle)
def _registry_plugin_from_item(item: dict[str, Any], source: Path) -> RegistryPlugin:
dependencies = tuple(
Dependency(
id=dep["id"],
constraint=dep.get("constraint"),
required=dep.get("required", True),
)
for dep in item.get("dependencies", [])
)
strategy = item.get("install_strategy", "manual")
if strategy not in VALID_STRATEGIES:
raise ValueError(f"{source}: invalid install_strategy for {item['id']}: {strategy}")
return RegistryPlugin(
id=item["id"],
name=item.get("name", item["id"]),
repo=item.get("repo"),
asset_patterns=tuple(item.get("asset_patterns", [])),
install_strategy=strategy,
category=item.get("category"),
dependencies=dependencies,
)
def _add_registry_plugin(plugins: dict[str, RegistryPlugin], plugin: RegistryPlugin, source: Path) -> None:
if plugin.id in plugins:
raise ValueError(f"{source}: duplicate plugin id: {plugin.id}")
plugins[plugin.id] = plugin
def load_registry(path: Path) -> Registry:
if not path.exists():
legacy_path = path.with_suffix(".toml")
if path.name == "plugins" and legacy_path.exists():
return load_registry(legacy_path)
return Registry()
data = _load_toml(path)
plugins: dict[str, RegistryPlugin] = {}
if path.is_dir():
for item_path in sorted(path.glob("*.toml")):
data = _load_toml(item_path)
_add_registry_plugin(plugins, _registry_plugin_from_item(data, item_path), item_path)
return Registry(plugins)
data = _load_toml(path)
for item in data.get("plugins", []):
dependencies = tuple(
Dependency(
id=dep["id"],
constraint=dep.get("constraint"),
required=dep.get("required", True),
)
for dep in item.get("dependencies", [])
)
strategy = item.get("install_strategy", "manual")
if strategy not in VALID_STRATEGIES:
raise ValueError(f"{path}: invalid install_strategy for {item['id']}: {strategy}")
plugin = RegistryPlugin(
id=item["id"],
name=item.get("name", item["id"]),
repo=item.get("repo"),
asset_patterns=tuple(item.get("asset_patterns", [])),
install_strategy=strategy,
category=item.get("category"),
dependencies=dependencies,
)
plugins[plugin.id] = plugin
_add_registry_plugin(plugins, _registry_plugin_from_item(item, path), path)
return Registry(plugins)
+2 -2
View File
@@ -35,7 +35,7 @@ def enable_disabled_plugin(
instance_path: Path,
state_root: Path,
plugin_id: str,
registry: str = "registry/plugins.toml",
registry: str = "registry/plugins",
lockfile: str | None = None,
repo: Path | None = None,
progress: Callable[[str], None] | None = None,
@@ -85,7 +85,7 @@ def enable_disabled_plugins(
instance_path: Path,
state_root: Path,
plugin_ids: list[str],
registry: str = "registry/plugins.toml",
registry: str = "registry/plugins",
lockfile: str | None = None,
repo: Path | None = None,
progress: Callable[[str], None] | None = None,
+25 -1
View File
@@ -65,6 +65,26 @@ def _asset_matches_patterns(name: str, patterns: tuple[str, ...]) -> bool:
return not patterns or any(fnmatch.fnmatch(name, pattern) for pattern in patterns)
def _expand_required_dependencies(selected_ids: set[str], registry: Registry, lockfile: Lockfile) -> set[str]:
locked_ids = {plugin.id for plugin in lockfile.plugins}
expanded = set(selected_ids)
pending = list(selected_ids)
while pending:
plugin_id = pending.pop()
registry_plugin = registry.get(plugin_id)
if not registry_plugin:
continue
for dependency in registry_plugin.dependencies:
if not dependency.required:
continue
if dependency.id not in locked_ids:
raise ValueError(f"{plugin_id}: required dependency is not locked for this instance: {dependency.id}")
if dependency.id not in expanded:
expanded.add(dependency.id)
pending.append(dependency.id)
return expanded
def create_plan(
*,
instance: str,
@@ -77,7 +97,11 @@ def create_plan(
selected: set[str] | None = None,
require_bootstrap: bool = True,
) -> tuple[dict[str, Any], Path]:
selected_ids = selected or {plugin.id for plugin in lockfile.plugins}
selected_ids = (
_expand_required_dependencies(selected, registry, lockfile)
if selected is not None
else {plugin.id for plugin in lockfile.plugins}
)
changes: list[dict[str, Any]] = []
warnings: list[str] = []
+1 -1
View File
@@ -269,7 +269,7 @@ class PluginHelperTui(App[int]):
lockfile = load_lockfile(self.repo_root / "locks" / f"{target.instance_name}.lock.toml")
report = installed_plugins_report(
installed_state=load_installed_state(target.state_root, target.instance_name),
registry=load_registry(self.repo_root / "registry" / "plugins.toml"),
registry=load_registry(self.repo_root / "registry" / "plugins"),
lockfile=lockfile,
)
self.plugin_rows = report["plugins"]