feat(plugins): discover packages and persist state
This commit is contained in:
78
app-instance/backend/beaver/plugins/state.py
Normal file
78
app-instance/backend/beaver/plugins/state.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Atomic state persistence for declarative plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .models import PluginSkillBinding, PluginState
|
||||
|
||||
|
||||
class PluginStateStore:
|
||||
def __init__(self, workspace: str | Path) -> None:
|
||||
self.workspace = Path(workspace)
|
||||
self.root = self.workspace / ".beaver" / "plugins"
|
||||
self.path = self.root / "state.json"
|
||||
|
||||
def list_plugins(self) -> list[PluginState]:
|
||||
return [
|
||||
PluginState.from_dict(plugin_id, payload if isinstance(payload, dict) else {})
|
||||
for plugin_id, payload in sorted(self._read_state().get("plugins", {}).items())
|
||||
]
|
||||
|
||||
def get_plugin(self, plugin_id: str) -> PluginState | None:
|
||||
payload = self._read_state().get("plugins", {}).get(plugin_id)
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
return PluginState.from_dict(plugin_id, payload)
|
||||
|
||||
def set_enabled(self, plugin_id: str, enabled: bool) -> PluginState:
|
||||
state = self.get_plugin(plugin_id) or PluginState(plugin_id=plugin_id)
|
||||
state.enabled = enabled
|
||||
if enabled and state.status == "discovered":
|
||||
state.status = "enabled"
|
||||
self.upsert_plugin(state)
|
||||
return state
|
||||
|
||||
def upsert_plugin(self, plugin_state: PluginState) -> None:
|
||||
state = self._read_state()
|
||||
plugins = state.setdefault("plugins", {})
|
||||
if not isinstance(plugins, dict):
|
||||
plugins = {}
|
||||
state["plugins"] = plugins
|
||||
plugins[plugin_state.plugin_id] = plugin_state.to_dict()
|
||||
self._write_state(state)
|
||||
|
||||
def update_skill_binding(
|
||||
self,
|
||||
plugin_id: str,
|
||||
skill_name: str,
|
||||
binding: PluginSkillBinding,
|
||||
) -> PluginState:
|
||||
state = self.get_plugin(plugin_id) or PluginState(plugin_id=plugin_id)
|
||||
state.skills[skill_name] = binding
|
||||
self.upsert_plugin(state)
|
||||
return state
|
||||
|
||||
def _read_state(self) -> dict[str, Any]:
|
||||
if not self.path.exists():
|
||||
return {"plugins": {}}
|
||||
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if not isinstance(payload, dict):
|
||||
return {"plugins": {}}
|
||||
plugins = payload.get("plugins")
|
||||
if not isinstance(plugins, dict):
|
||||
payload["plugins"] = {}
|
||||
return payload
|
||||
|
||||
def _write_state(self, state: dict[str, Any]) -> None:
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = self.path.with_name("state.json.tmp")
|
||||
with tmp_path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(state, handle, ensure_ascii=False, sort_keys=True, indent=2)
|
||||
handle.write("\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(tmp_path, self.path)
|
||||
Reference in New Issue
Block a user