use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct Secrets {
values: HashMap<String, String>,
}
impl Secrets {
pub fn load() -> Self {
let home = std::env::var("HOME").unwrap_or_default();
let secrets_path = PathBuf::from(home).join(".exomonad/secrets");
if let Ok(content) = std::fs::read_to_string(&secrets_path) {
tracing::info!(path = %secrets_path.display(), "Loaded secrets");
let values = parse_env_file(&content);
for (key, value) in &values {
if std::env::var(key).is_err() {
std::env::set_var(key, value);
tracing::debug!(key = %key, "Exported secret to environment");
}
}
Self { values }
} else {
tracing::debug!(
path = %secrets_path.display(),
"No secrets file, using env vars"
);
Self::default()
}
}
pub fn get(&self, key: &str) -> Option<String> {
self.values
.get(key)
.cloned()
.or_else(|| std::env::var(key).ok())
}
pub fn github_token(&self) -> Option<String> {
self.get("GITHUB_TOKEN")
}
pub fn anthropic_api_key(&self) -> Option<String> {
self.get("ANTHROPIC_API_KEY")
}
}
fn parse_env_file(content: &str) -> HashMap<String, String> {
content
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.filter_map(|line| {
let mut parts = line.splitn(2, '=');
let key = parts.next()?.trim();
let value = parts.next()?.trim();
let value = value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.unwrap_or(value);
Some((key.to_string(), value.to_string()))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_env_file() {
let content = r#"
# Comment
GITHUB_TOKEN=ghp_123
ANTHROPIC_API_KEY=sk-ant-456
# Another comment
EMPTY=
QUOTED="value with spaces"
"#;
let parsed = parse_env_file(content);
assert_eq!(parsed.get("GITHUB_TOKEN"), Some(&"ghp_123".to_string()));
assert_eq!(
parsed.get("ANTHROPIC_API_KEY"),
Some(&"sk-ant-456".to_string())
);
assert_eq!(parsed.get("EMPTY"), Some(&"".to_string()));
assert_eq!(parsed.get("QUOTED"), Some(&"value with spaces".to_string()));
}
#[test]
fn test_secrets_fallback_to_env() {
let secrets = Secrets::default();
let _ = secrets.get("GITHUB_TOKEN");
}
}