use std::path::PathBuf;
use crate::models::ModelRegistry;
const REGISTRY_URL: &str =
"https://raw.githubusercontent.com/ayshptk/harness-cli/main/models.toml";
const TTL_SECS: u64 = 86400;
const FETCH_TIMEOUT_SECS: u64 = 5;
pub fn canonical_path() -> Option<PathBuf> {
dirs::home_dir().map(|d| d.join(".harness").join("models.toml"))
}
pub fn load_canonical() -> ModelRegistry {
let path = match canonical_path() {
Some(p) => p,
None => {
tracing::debug!("cannot determine home directory, using builtin registry");
return ModelRegistry::builtin();
}
};
if path.exists() && !is_stale(&path) {
if let Some(reg) = load_from_disk(&path) {
return reg;
}
}
match fetch_and_cache(&path) {
Ok(reg) => return reg,
Err(e) => {
tracing::debug!("failed to fetch models registry: {e}");
}
}
if path.exists() {
if let Some(reg) = load_from_disk(&path) {
tracing::debug!("using stale cached registry");
return reg;
}
}
tracing::debug!("using builtin registry");
ModelRegistry::builtin()
}
pub fn force_update() -> Result<String, String> {
let path = canonical_path().ok_or("cannot determine home directory")?;
match fetch_and_cache(&path) {
Ok(_) => Ok(format!("Updated registry at {}", path.display())),
Err(e) => Err(format!("failed to fetch: {e}")),
}
}
fn is_stale(path: &std::path::Path) -> bool {
let metadata = match std::fs::metadata(path) {
Ok(m) => m,
Err(_) => return true,
};
let modified = match metadata.modified() {
Ok(t) => t,
Err(_) => return true,
};
let age = std::time::SystemTime::now()
.duration_since(modified)
.unwrap_or_default();
age.as_secs() > TTL_SECS
}
fn load_from_disk(path: &std::path::Path) -> Option<ModelRegistry> {
let content = std::fs::read_to_string(path).ok()?;
match ModelRegistry::from_toml(&content) {
Ok(reg) => Some(reg),
Err(e) => {
tracing::warn!("failed to parse cached registry at {}: {e}", path.display());
None
}
}
}
fn fetch_and_cache(path: &std::path::Path) -> Result<ModelRegistry, String> {
let body = fetch_registry_content()?;
let reg = ModelRegistry::from_toml(&body)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir failed: {e}"))?;
}
let tmp_path = path.with_extension("toml.tmp");
std::fs::write(&tmp_path, &body).map_err(|e| format!("write failed: {e}"))?;
std::fs::rename(&tmp_path, path).map_err(|e| format!("rename failed: {e}"))?;
tracing::debug!("cached registry at {}", path.display());
Ok(reg)
}
fn fetch_registry_content() -> Result<String, String> {
let agent = ureq::Agent::config_builder()
.timeout_global(Some(std::time::Duration::from_secs(FETCH_TIMEOUT_SECS)))
.build()
.new_agent();
let body = agent
.get(REGISTRY_URL)
.call()
.map_err(|e| format!("HTTP request failed: {e}"))?
.body_mut()
.read_to_string()
.map_err(|e| format!("failed to read response body: {e}"))?;
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_path_is_under_home() {
if let Some(path) = canonical_path() {
assert!(path.to_string_lossy().contains(".harness"));
assert!(path.to_string_lossy().ends_with("models.toml"));
}
}
#[test]
fn is_stale_missing_file() {
assert!(is_stale(std::path::Path::new("/nonexistent/file")));
}
#[test]
fn load_from_disk_valid() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
r#"
[models.test]
description = "Test Model"
provider = "test"
claude = "test-id"
"#,
)
.unwrap();
let reg = load_from_disk(tmp.path());
assert!(reg.is_some());
assert!(reg.unwrap().models.contains_key("test"));
}
#[test]
fn load_from_disk_invalid_returns_none() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "{{{{ not toml").unwrap();
assert!(load_from_disk(tmp.path()).is_none());
}
#[test]
fn load_canonical_returns_something() {
let reg = load_canonical();
assert!(!reg.models.is_empty());
}
}