use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
#[cfg(test)]
pub(crate) static HOME_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub fn dir() -> Option<PathBuf> {
dir_from(std::env::var_os("ZAKURO_HOME"), std::env::var_os("HOME"))
}
pub fn dir_from(
zakuro_home: Option<std::ffi::OsString>,
home: Option<std::ffi::OsString>,
) -> Option<PathBuf> {
if let Some(h) = zakuro_home {
if !h.is_empty() {
return Some(PathBuf::from(h));
}
}
home.map(|h| PathBuf::from(h).join(".zakuro"))
}
pub const PROD_API_URL: &str = "https://hub.zakuro-ai.com";
pub const STAGING_API_URL: &str = "https://stg.api.zakuro-ai.com";
pub fn default_api_url() -> String {
if let Ok(u) = std::env::var("ZAKURO_API_URL") {
if !u.trim().is_empty() {
return u;
}
}
match std::env::var("ZAKURO_ENV")
.unwrap_or_default()
.to_lowercase()
.as_str()
{
"staging" | "stg" | "stage" => STAGING_API_URL.to_string(),
_ => PROD_API_URL.to_string(),
}
}
pub fn path() -> Option<PathBuf> {
dir().map(|d| d.join("credentials"))
}
pub fn parse(text: &str) -> HashMap<String, String> {
let mut m = HashMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let line = line.strip_prefix("export ").unwrap_or(line);
if let Some((k, v)) = line.split_once('=') {
m.insert(k.trim().to_string(), v.trim().to_string());
}
}
m
}
pub fn save(api_key: &str, api_url: Option<&str>) -> std::io::Result<()> {
let d = dir().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no HOME"))?;
std::fs::create_dir_all(&d)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700)).ok();
}
let p = d.join("credentials");
let mut body = format!("api_key={}\n", api_key);
if let Some(u) = api_url {
body.push_str(&format!("api_url={}\n", u));
}
let mut f = std::fs::File::create(&p)?;
f.write_all(body.as_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600)).ok();
}
Ok(())
}
pub fn load_into_env() {
if let Some(p) = path() {
if let Ok(text) = std::fs::read_to_string(&p) {
let m = parse(&text);
set_if_unset("ZAKURO_API_KEY", m.get("api_key"));
set_if_unset("ZAKURO_API_URL", m.get("api_url"));
}
}
if let Some(d) = dir() {
if let Ok(text) = std::fs::read_to_string(d.join("env")) {
let m = parse(&text);
set_if_unset("ZAKURO_API_KEY", m.get("ZAKURO_API_KEY"));
set_if_unset("ZAKURO_API_URL", m.get("ZAKURO_API_URL"));
}
}
}
fn set_if_unset(var: &str, value: Option<&String>) {
if std::env::var(var).is_err() {
if let Some(v) = value {
if !v.is_empty() {
std::env::set_var(var, v);
}
}
}
}
pub fn mesh_peer_key_path() -> Option<PathBuf> {
dir().map(|d| d.join("mesh_peer_key"))
}
pub fn load_mesh_peer_key() -> Option<String> {
let p = mesh_peer_key_path()?;
let text = std::fs::read_to_string(p).ok()?;
let k = text.trim();
if k.is_empty() {
None
} else {
Some(k.to_string())
}
}
pub fn save_mesh_peer_key(key: &str) -> std::io::Result<()> {
let d = dir().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no HOME"))?;
std::fs::create_dir_all(&d)?;
let p = d.join("mesh_peer_key");
let mut f = std::fs::File::create(&p)?;
f.write_all(key.trim().as_bytes())?;
f.write_all(b"\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600)).ok();
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn parse_prefers_env_else_file() {
let txt = "api_key=zk_1_abc\napi_url=http://x\n";
let m = super::parse(txt);
assert_eq!(m.get("api_key").unwrap(), "zk_1_abc");
assert_eq!(m.get("api_url").unwrap(), "http://x");
}
#[test]
fn parse_strips_export_prefix() {
let m = super::parse("export ZAKURO_API_URL=https://stg\n# c\nZAKURO_API_KEY=zk_1_a\n");
assert_eq!(m.get("ZAKURO_API_URL").unwrap(), "https://stg");
assert_eq!(m.get("ZAKURO_API_KEY").unwrap(), "zk_1_a");
}
#[test]
fn staging_points_at_stg_api_not_the_retired_stg_my() {
assert_eq!(
super::STAGING_API_URL,
"https://stg.api.zakuro-ai.com",
"staging must address stg.api, the machine host; stg-my is decommissioned"
);
assert_eq!(
super::PROD_API_URL,
"https://hub.zakuro-ai.com",
"prod must name a host that resolves; my.zakuro-ai.com is NXDOMAIN"
);
}
}