1use eyre::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Serialize, Deserialize, Clone, Debug)]
15pub struct AgentEntry {
16 pub name: String,
17 pub id: String,
19 pub transport: String,
21 pub endpoint: String,
23 #[serde(default)]
25 pub capabilities: Vec<String>,
26 pub registered_at: u64,
28 pub last_seen: u64,
30}
31
32pub fn registry_path() -> PathBuf {
34 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
35 PathBuf::from(home).join(".rz").join("registry.json")
36}
37
38pub fn load() -> Result<HashMap<String, AgentEntry>> {
40 let path = registry_path();
41 if !path.exists() {
42 return Ok(HashMap::new());
43 }
44 let data = fs::read_to_string(&path)
45 .wrap_err_with(|| format!("failed to read {}", path.display()))?;
46 let map: HashMap<String, AgentEntry> =
47 serde_json::from_str(&data).wrap_err("failed to parse registry.json")?;
48 Ok(map)
49}
50
51pub fn save(registry: &HashMap<String, AgentEntry>) -> Result<()> {
53 let path = registry_path();
54 if let Some(parent) = path.parent() {
55 fs::create_dir_all(parent)
56 .wrap_err_with(|| format!("failed to create {}", parent.display()))?;
57 }
58 let json = serde_json::to_string_pretty(registry)
59 .wrap_err("failed to serialize registry")?;
60
61 let tmp = path.with_extension("json.tmp");
63 fs::write(&tmp, json.as_bytes())
64 .wrap_err_with(|| format!("failed to write {}", tmp.display()))?;
65 fs::rename(&tmp, &path)
66 .wrap_err_with(|| format!("failed to rename {} -> {}", tmp.display(), path.display()))?;
67 Ok(())
68}
69
70pub fn register(entry: AgentEntry) -> Result<()> {
72 let mut reg = load()?;
73 reg.insert(entry.name.clone(), entry);
74 save(®)
75}
76
77pub fn deregister(name: &str) -> Result<()> {
79 let mut reg = load()?;
80 reg.remove(name);
81 save(®)
82}
83
84pub fn lookup(name: &str) -> Result<Option<AgentEntry>> {
86 let reg = load()?;
87 Ok(reg.get(name).cloned())
88}
89
90pub fn list_all() -> Result<Vec<AgentEntry>> {
92 let reg = load()?;
93 Ok(reg.into_values().collect())
94}
95
96pub fn cleanup_stale(max_age_secs: u64) -> Result<usize> {
99 let mut reg = load()?;
100 let now_ms = SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .unwrap_or_default()
103 .as_millis() as u64;
104 let cutoff = now_ms.saturating_sub(max_age_secs * 1000);
105
106 let before = reg.len();
107 reg.retain(|_, entry| entry.last_seen >= cutoff);
108 let removed = before - reg.len();
109
110 if removed > 0 {
111 save(®)?;
112 }
113 Ok(removed)
114}
115
116pub fn touch(name: &str) -> Result<()> {
118 let mut reg = load()?;
119 if let Some(entry) = reg.get_mut(name) {
120 entry.last_seen = SystemTime::now()
121 .duration_since(UNIX_EPOCH)
122 .unwrap_or_default()
123 .as_millis() as u64;
124 save(®)?;
125 }
126 Ok(())
127}