use std::path::Path;
use super::util::is_proxy_reachable;
pub(crate) fn install_pi_env(home: &Path, port: u16, quiet: bool, force: bool) {
install_pi_env_at(&home.join(".pi/agent"), port, quiet, force);
}
pub(crate) fn uninstall_pi_env(home: &Path, quiet: bool) {
uninstall_pi_env_at(&home.join(".pi/agent"), quiet);
}
pub(crate) fn install_pi_env_at(agent_dir: &Path, port: u16, quiet: bool, force: bool) {
use crate::core::config::{is_local_proxy_url, normalize_url_opt};
if !agent_dir.exists() {
return;
}
if !is_proxy_reachable(port) {
if !quiet {
println!(" Skipping Pi proxy env (proxy not running on port {port})");
}
return;
}
let base = format!("http://127.0.0.1:{port}");
let models_path = agent_dir.join("models.json");
let existing = std::fs::read_to_string(&models_path).unwrap_or_default();
let mut doc: serde_json::Value = if existing.trim().is_empty() {
serde_json::json!({})
} else {
match crate::core::jsonc::parse_jsonc(&existing) {
Ok(v) => v,
Err(_) => return,
}
};
let mut changed = false;
let mut kept_custom: Vec<String> = Vec::new();
for (provider, proxy_url) in [
("anthropic", base.clone()),
("openai", format!("{base}/v1")),
] {
let current = pi_provider_base_url(&doc, provider).to_string();
if current == proxy_url {
continue;
}
if !force
&& let Some(custom) = normalize_url_opt(¤t)
&& !is_local_proxy_url(&custom)
{
kept_custom.push(format!("{provider} → {custom}"));
continue;
}
set_pi_provider_base_url(&mut doc, provider, &proxy_url);
changed = true;
}
if changed {
let out = serde_json::to_string_pretty(&doc).unwrap_or_default();
let _ = std::fs::write(&models_path, out + "\n");
if !quiet {
println!(
" Configured Pi providers (anthropic/openai) → proxy in ~/.pi/agent/models.json"
);
}
}
if !quiet && !kept_custom.is_empty() {
eprintln!(
" \u{26a0} Pi: kept custom endpoint(s) {}; use `lean-ctx proxy enable --force` to override.",
kept_custom.join(", ")
);
}
}
pub(crate) fn uninstall_pi_env_at(agent_dir: &Path, quiet: bool) {
use crate::core::config::is_local_proxy_url;
let models_path = agent_dir.join("models.json");
let existing = match std::fs::read_to_string(&models_path) {
Ok(s) if !s.trim().is_empty() => s,
_ => return,
};
let mut doc: serde_json::Value = match crate::core::jsonc::parse_jsonc(&existing) {
Ok(v) => v,
Err(_) => return,
};
let mut changed = false;
for provider in ["anthropic", "openai"] {
if is_local_proxy_url(pi_provider_base_url(&doc, provider))
&& remove_pi_provider_base_url(&mut doc, provider)
{
changed = true;
}
}
if changed {
let out = serde_json::to_string_pretty(&doc).unwrap_or_default();
let _ = std::fs::write(&models_path, out + "\n");
if !quiet {
println!(" \u{2713} Removed Pi proxy endpoints from ~/.pi/agent/models.json");
}
}
}
pub(crate) fn pi_provider_base_url<'a>(doc: &'a serde_json::Value, provider: &str) -> &'a str {
doc.get("providers")
.and_then(|p| p.get(provider))
.and_then(|p| p.get("baseUrl"))
.and_then(serde_json::Value::as_str)
.unwrap_or("")
}
pub(crate) fn set_pi_provider_base_url(doc: &mut serde_json::Value, provider: &str, url: &str) {
let Some(root) = doc.as_object_mut() else {
return;
};
let providers = root
.entry("providers")
.or_insert_with(|| serde_json::json!({}));
let Some(providers) = providers.as_object_mut() else {
return;
};
let entry = providers
.entry(provider.to_string())
.or_insert_with(|| serde_json::json!({}));
if let Some(entry) = entry.as_object_mut() {
entry.insert(
"baseUrl".to_string(),
serde_json::Value::String(url.to_string()),
);
}
}
pub(crate) fn remove_pi_provider_base_url(doc: &mut serde_json::Value, provider: &str) -> bool {
let Some(root) = doc.as_object_mut() else {
return false;
};
let Some(providers) = root.get_mut("providers").and_then(|p| p.as_object_mut()) else {
return false;
};
let Some(entry) = providers.get_mut(provider).and_then(|p| p.as_object_mut()) else {
return false;
};
if entry.remove("baseUrl").is_none() {
return false;
}
if entry.is_empty() {
providers.remove(provider);
}
if providers.is_empty() {
root.remove("providers");
}
true
}