use std::path::PathBuf;
use super::{
ContentRecord, OverwriteChoice, UpdateState, bytes_sha256, colors, confirm_action,
confirm_overwrite, fetch_file, fetch_manifest, file_sha256, icons, now_iso, print_diff,
registry_base_url, resolve_registry_url,
};
use crate::cli::{CRT_DRAW_MS, heading, theme};
pub(super) fn providers_local_path(config_path: &str) -> PathBuf {
if let Ok(content) = std::fs::read_to_string(config_path)
&& let Ok(config) = content.parse::<toml::Value>()
&& let Some(path) = config.get("providers_file").and_then(|v| v.as_str())
{
return PathBuf::from(path);
}
super::roboticus_home().join("providers.toml")
}
pub(super) async fn apply_providers_update(
yes: bool,
registry_url: &str,
config_path: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let (DIM, BOLD, _, GREEN, YELLOW, _, _, RESET, MONO) = colors();
let (OK, _, WARN, DETAIL, _) = icons();
let client = super::http_client()?;
println!("\n {BOLD}Provider Configs{RESET}\n");
let manifest = match fetch_manifest(&client, registry_url).await {
Ok(m) => m,
Err(e) => {
println!(" {WARN} Could not fetch registry manifest: {e}");
return Ok(false);
}
};
let base_url = registry_base_url(registry_url);
let remote_content = match fetch_file(&client, &base_url, &manifest.packs.providers.path).await
{
Ok(c) => c,
Err(e) => {
println!(" {WARN} Could not fetch providers.toml: {e}");
return Ok(false);
}
};
let remote_hash = bytes_sha256(remote_content.as_bytes());
let state = UpdateState::load();
let local_path = providers_local_path(config_path);
let local_exists = local_path.exists();
let local_content = if local_exists {
std::fs::read_to_string(&local_path).unwrap_or_default()
} else {
String::new()
};
if local_exists {
let local_hash = bytes_sha256(local_content.as_bytes());
if local_hash == remote_hash {
println!(" {OK} Provider configs are up to date");
return Ok(false);
}
}
let user_modified = if let Some(ref record) = state.installed_content.providers {
if local_exists {
let current_hash = file_sha256(&local_path).unwrap_or_default();
current_hash != record.sha256
} else {
false
}
} else {
local_exists
};
if !local_exists {
println!(" {GREEN}+ New provider configuration available{RESET}");
print_diff("", &remote_content);
} else if user_modified {
println!(" {YELLOW}Provider config has been modified locally{RESET}");
println!(" Changes from registry:");
print_diff(&local_content, &remote_content);
} else {
println!(" Updated provider configuration available");
print_diff(&local_content, &remote_content);
}
println!();
if user_modified {
match confirm_overwrite("providers config") {
OverwriteChoice::Overwrite => {}
OverwriteChoice::Backup => {
let backup = local_path.with_extension("toml.bak");
std::fs::copy(&local_path, &backup)?;
println!(" {DETAIL} Backed up to {}", backup.display());
}
OverwriteChoice::Skip => {
println!(" Skipped.");
return Ok(false);
}
}
} else if !yes && !confirm_action("Apply provider updates?", true) {
println!(" Skipped.");
return Ok(false);
}
if let Some(parent) = local_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&local_path, &remote_content)?;
let mut state = UpdateState::load();
state.installed_content.providers = Some(ContentRecord {
version: manifest.version.clone(),
sha256: remote_hash,
installed_at: now_iso(),
});
state.last_check = now_iso();
state
.save()
.inspect_err(
|e| tracing::warn!(error = %e, "failed to save update state after provider install"),
)
.ok();
println!(" {OK} Provider configs updated to v{}", manifest.version);
Ok(true)
}
pub async fn cmd_update_providers(
yes: bool,
registry_url_override: Option<&str>,
config_path: &str,
hygiene_fn: Option<&super::HygieneFn>,
) -> Result<(), Box<dyn std::error::Error>> {
heading("Provider Config Update");
let registry_url = resolve_registry_url(registry_url_override, config_path);
apply_providers_update(yes, ®istry_url, config_path).await?;
super::run_oauth_storage_maintenance();
super::run_mechanic_checks_maintenance(config_path, hygiene_fn);
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::EnvGuard;
#[test]
fn local_path_helpers_fallback_when_config_missing() {
let p = providers_local_path("/no/such/file.toml");
assert!(p.ends_with("providers.toml"));
}
#[serial_test::serial]
#[tokio::test]
async fn apply_providers_update_fetches_and_writes_local_file() {
let temp = tempfile::tempdir().unwrap();
let _home_guard = EnvGuard::set("HOME", temp.path().to_str().unwrap());
let config_path = temp.path().join("roboticus.toml");
let providers_path = temp.path().join("providers.toml");
std::fs::write(
&config_path,
format!(
"providers_file = \"{}\"\n",
providers_path.display().to_string().replace('\\', "/")
),
)
.unwrap();
let providers = "[providers.openai]\nurl = \"https://api.openai.com\"\n".to_string();
let (registry_url, handle) = crate::cli::update::tests_support::start_mock_registry(
providers.clone(),
"# hello\nbody\n".to_string(),
)
.await;
let changed = apply_providers_update(true, ®istry_url, config_path.to_str().unwrap())
.await
.unwrap();
assert!(changed);
assert_eq!(std::fs::read_to_string(&providers_path).unwrap(), providers);
let changed_second =
apply_providers_update(true, ®istry_url, config_path.to_str().unwrap())
.await
.unwrap();
assert!(!changed_second);
handle.abort();
}
}