use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::output::CliError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Remote {
pub peer_id: String,
pub addrs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Remotes {
#[serde(flatten)]
pub peers: BTreeMap<String, Remote>,
}
impl Remotes {
pub fn load() -> Self {
Self::load_from_path(&Self::default_path())
}
fn load_from_path(path: &Path) -> Self {
if !path.exists() {
return Self::default();
}
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self) -> Result<(), CliError> {
let path = Self::default_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| CliError::internal(format!("create dir: {e}")))?;
}
let json = serde_json::to_string_pretty(self)
.map_err(|e| CliError::internal(format!("serialize remotes: {e}")))?;
std::fs::write(&path, json)
.map_err(|e| CliError::internal(format!("write remotes: {e}")))?;
Ok(())
}
fn default_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".void")
.join("remotes.json")
}
pub fn add(&mut self, name: &str, remote: Remote) -> Result<(), CliError> {
if self.peers.contains_key(name) {
return Err(CliError::internal(format!(
"remote '{name}' already exists — use `void remote remove {name}` first"
)));
}
self.peers.insert(name.to_string(), remote);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Result<(), CliError> {
if self.peers.remove(name).is_none() {
return Err(CliError::internal(format!("remote '{name}' not found")));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ResolvedRemote {
pub name: String,
pub peer_id: String,
pub addr: String,
pub label: Option<String>,
}
pub fn resolve_remote(
name: &str,
repo_config: Option<&void_core::config::Config>,
) -> Result<ResolvedRemote, CliError> {
if name.starts_with('/') {
let (addr, peer_id) = parse_remote_addr(name)?;
return Ok(ResolvedRemote {
name: "inline".to_string(),
peer_id,
addr,
label: None,
});
}
if let Some(cfg) = repo_config {
if let Some(remote) = cfg.remote.get(name) {
if let (Some(pid), Some(a)) = (remote.peer_id.as_deref(), remote.addrs.first()) {
return Ok(ResolvedRemote {
name: name.to_string(),
peer_id: pid.to_string(),
addr: a.clone(),
label: remote.label.clone(),
});
}
if let Some(ref multiaddr) = remote.peer_multiaddr {
if let Some(idx) = multiaddr.rfind("/p2p/") {
return Ok(ResolvedRemote {
name: name.to_string(),
peer_id: multiaddr[idx + 5..].to_string(),
addr: multiaddr[..idx].to_string(),
label: remote.label.clone(),
});
}
}
}
}
let global = Remotes::load();
if let Some(remote) = global.peers.get(name) {
if let Some(addr) = remote.addrs.first() {
return Ok(ResolvedRemote {
name: name.to_string(),
peer_id: remote.peer_id.clone(),
addr: addr.clone(),
label: remote.label.clone(),
});
}
}
Err(CliError::not_found(format!("remote '{name}' not found")))
}
pub fn resolve_all_remotes(
repo_config: Option<&void_core::config::Config>,
) -> Vec<ResolvedRemote> {
let mut result = Vec::new();
let mut seen = std::collections::HashSet::new();
if let Some(cfg) = repo_config {
for name in cfg.remote.keys() {
if let Ok(resolved) = resolve_remote(name, repo_config) {
seen.insert(name.clone());
result.push(resolved);
}
}
}
let global = Remotes::load();
for (name, remote) in &global.peers {
if !seen.contains(name) {
if let Some(addr) = remote.addrs.first() {
result.push(ResolvedRemote {
name: name.clone(),
peer_id: remote.peer_id.clone(),
addr: addr.clone(),
label: remote.label.clone(),
});
}
}
}
result
}
pub fn parse_remote_addr(addr_str: &str) -> Result<(String, String), CliError> {
let p2p_idx = addr_str.rfind("/p2p/").ok_or_else(|| {
CliError::invalid_args(format!(
"remote address must include /p2p/<peer_id>: {addr_str}"
))
})?;
let addr = addr_str[..p2p_idx].to_string();
let peer_id = addr_str[p2p_idx + 5..].to_string();
peer_id.parse::<libp2p::PeerId>().map_err(|e| {
CliError::invalid_args(format!("invalid peer ID in address: {e}"))
})?;
addr.parse::<libp2p::Multiaddr>().map_err(|e| {
CliError::invalid_args(format!("invalid multiaddr: {e}"))
})?;
Ok((addr, peer_id))
}