1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::AppError;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Profile {
10 pub client_id: String,
11 #[serde(default = "default_tenant")]
12 pub tenant: String,
13}
14
15fn default_tenant() -> String {
16 "organizations".into()
17}
18
19#[derive(Debug, Default, Serialize, Deserialize)]
20struct ConfigFile {
21 #[serde(default)]
22 active_profile: Option<String>,
23 #[serde(default)]
24 profiles: BTreeMap<String, Profile>,
25}
26
27pub fn path() -> PathBuf {
28 if let Some(base) = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
29 PathBuf::from(base).join("teams").join("config.toml")
30 } else {
31 config_base().join("teams").join("config.toml")
32 }
33}
34
35pub fn exists() -> bool {
36 path().is_file()
37}
38
39pub fn save(profile_name: &str, profile: Profile) -> Result<PathBuf, AppError> {
40 let path = path();
41 let mut config = read_file()?;
42 config.profiles.insert(profile_name.to_string(), profile);
43 config.active_profile = Some(profile_name.to_string());
44 let parent = path
45 .parent()
46 .ok_or_else(|| AppError::Unexpected("configuration path has no parent".into()))?;
47 std::fs::create_dir_all(parent)?;
48 let body = toml::to_string_pretty(&config).map_err(|e| AppError::Unexpected(e.to_string()))?;
49 std::fs::write(&path, body)?;
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::PermissionsExt;
53 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
54 }
55 Ok(path)
56}
57
58pub fn load(requested: Option<&str>) -> Result<(String, Profile), AppError> {
59 let config = read_file()?;
60 let name = requested
61 .map(str::to_owned)
62 .or_else(|| std::env::var("TEAMS_PROFILE").ok())
63 .or(config.active_profile)
64 .unwrap_or_else(|| "default".into());
65 let stored = config.profiles.get(&name);
66 let client_id = std::env::var("TEAMS_CLIENT_ID")
67 .ok()
68 .or_else(|| stored.map(|profile| profile.client_id.clone()))
69 .ok_or_else(|| {
70 AppError::InvalidInput(format!(
71 "profile '{name}' is not configured; run `teams init` or set TEAMS_CLIENT_ID"
72 ))
73 })?;
74 let tenant = std::env::var("TEAMS_TENANT")
75 .ok()
76 .or_else(|| stored.map(|profile| profile.tenant.clone()))
77 .unwrap_or_else(default_tenant);
78 let profile = Profile { client_id, tenant };
79 Ok((name, profile))
80}
81
82#[cfg(windows)]
83fn config_base() -> PathBuf {
84 dirs::config_dir().unwrap_or_else(|| PathBuf::from(".config"))
85}
86
87#[cfg(not(windows))]
88fn config_base() -> PathBuf {
89 dirs::home_dir()
90 .map(|home| home.join(".config"))
91 .unwrap_or_else(|| PathBuf::from(".config"))
92}
93
94pub fn configured_profile(requested: Option<&str>) -> Option<(String, Profile)> {
95 load(requested).ok()
96}
97
98fn read_file() -> Result<ConfigFile, AppError> {
99 match std::fs::read_to_string(path()) {
100 Ok(body) => toml::from_str(&body)
101 .map_err(|e| AppError::InvalidInput(format!("cannot parse {}: {e}", path().display()))),
102 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ConfigFile::default()),
103 Err(e) => Err(e.into()),
104 }
105}