Initial reyna-cli implementation
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
DEFAULT_REGISTRY_PATH = Path.home() / ".hermes" / "local_devices.yaml"
|
||||
CACHE_DIR = Path(os.environ.get("REYNA_CLI_CACHE_DIR", Path.home() / ".cache" / "reyna-cli"))
|
||||
|
||||
|
||||
class Device(BaseModel):
|
||||
id: str
|
||||
display_name: str = ""
|
||||
type: str = ""
|
||||
status: str = ""
|
||||
host: str = ""
|
||||
url: str = ""
|
||||
fallback_url: str = ""
|
||||
reserved_ip: str = ""
|
||||
ports: Dict[str, Any] = Field(default_factory=dict)
|
||||
capabilities: List[str] = Field(default_factory=list)
|
||||
preferred_actions: Dict[str, str] = Field(default_factory=dict)
|
||||
notes: List[str] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.id
|
||||
|
||||
def candidate_urls(self) -> List[str]:
|
||||
urls: List[str] = []
|
||||
for url in (self.url, self.fallback_url):
|
||||
if url and url not in urls:
|
||||
urls.append(url)
|
||||
return urls
|
||||
|
||||
def public_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"display_name": self.display_name,
|
||||
"type": self.type,
|
||||
"status": self.status,
|
||||
"host": self.host,
|
||||
"url": self.url,
|
||||
"fallback_url": self.fallback_url,
|
||||
"reserved_ip": self.reserved_ip,
|
||||
"ports": self.ports,
|
||||
"capabilities": self.capabilities,
|
||||
"preferred_actions": self.preferred_actions,
|
||||
"notes": self.notes,
|
||||
}
|
||||
|
||||
|
||||
class Registry(BaseModel):
|
||||
path: Optional[str] = None
|
||||
version: int = 1
|
||||
updated: str = ""
|
||||
aliases: Dict[str, str] = Field(default_factory=dict)
|
||||
devices: List[Device] = Field(default_factory=list)
|
||||
|
||||
|
||||
def _device_from_mapping(device_id: str, raw: Dict[str, Any]) -> Device:
|
||||
data = dict(raw or {})
|
||||
data["id"] = device_id
|
||||
return Device(**data)
|
||||
|
||||
|
||||
def load_registry(path: Optional[Path] = None) -> Registry:
|
||||
paths_to_try: List[Path] = []
|
||||
if path:
|
||||
paths_to_try.append(Path(path).expanduser())
|
||||
env_path = os.environ.get("REYNA_DEVICES_REGISTRY")
|
||||
if env_path:
|
||||
paths_to_try.append(Path(env_path).expanduser())
|
||||
paths_to_try.extend([DEFAULT_REGISTRY_PATH, Path.cwd() / "local_devices.yaml"])
|
||||
|
||||
seen: set[Path] = set()
|
||||
for p in paths_to_try:
|
||||
p = p.expanduser()
|
||||
if p in seen:
|
||||
continue
|
||||
seen.add(p)
|
||||
if not p.exists():
|
||||
continue
|
||||
with p.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
devices_raw = data.get("devices", {}) or {}
|
||||
devices: List[Device] = []
|
||||
if isinstance(devices_raw, dict):
|
||||
devices = [_device_from_mapping(k, v) for k, v in devices_raw.items()]
|
||||
elif isinstance(devices_raw, list):
|
||||
for item in devices_raw:
|
||||
if isinstance(item, dict):
|
||||
device_id = item.get("id") or item.get("name") or item.get("display_name")
|
||||
if device_id:
|
||||
devices.append(_device_from_mapping(str(device_id), item))
|
||||
return Registry(
|
||||
path=str(p),
|
||||
version=data.get("version", 1),
|
||||
updated=str(data.get("updated", "")),
|
||||
aliases=data.get("aliases", {}) or {},
|
||||
devices=devices,
|
||||
)
|
||||
return Registry(path=None)
|
||||
|
||||
|
||||
def get_device(name: str, registry: Optional[Registry] = None) -> Optional[Device]:
|
||||
registry = registry or load_registry()
|
||||
normalized = name.strip()
|
||||
shorthand = {
|
||||
"screen": "esp32_screen",
|
||||
"esp32": "esp32_screen",
|
||||
"iphone": "iphone_mcp",
|
||||
"phone": "iphone_mcp",
|
||||
"arm": "robot_arm",
|
||||
}
|
||||
normalized = shorthand.get(normalized, normalized)
|
||||
for device in registry.devices:
|
||||
if normalized in {device.id, device.host, device.display_name}:
|
||||
return device
|
||||
return None
|
||||
|
||||
|
||||
def cache_path_for(device_id: str) -> Path:
|
||||
return CACHE_DIR / "devices" / f"{device_id}-tools.json"
|
||||
Reference in New Issue
Block a user