chub_core/team/
profiles.rs1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{Error, Result};
7use crate::team::project::project_chub_dir;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Profile {
12 pub name: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub extends: Option<String>,
15 #[serde(default)]
16 pub description: Option<String>,
17 #[serde(default)]
18 pub pins: Vec<String>,
19 #[serde(default)]
20 pub context: Vec<String>,
21 #[serde(default)]
22 pub rules: Vec<String>,
23}
24
25#[derive(Debug, Clone)]
27pub struct ResolvedProfile {
28 pub name: String,
29 pub description: Option<String>,
30 pub pins: Vec<String>,
31 pub context: Vec<String>,
32 pub rules: Vec<String>,
33}
34
35fn profiles_dir() -> Option<PathBuf> {
36 project_chub_dir().map(|d| d.join("profiles"))
37}
38
39pub fn load_profile(name: &str) -> Result<Profile> {
41 let dir = profiles_dir().ok_or_else(|| {
42 Error::Config("No .chub/ directory found. Run `chub init` first.".to_string())
43 })?;
44
45 let path = dir.join(format!("{}.yaml", name));
46 if !path.exists() {
47 let alt = dir.join(format!("{}.yml", name));
48 if alt.exists() {
49 let raw = fs::read_to_string(&alt)?;
50 return serde_yaml::from_str(&raw).map_err(|e| Error::Config(e.to_string()));
51 }
52 return Err(Error::Config(format!("Profile \"{}\" not found.", name)));
53 }
54
55 let raw = fs::read_to_string(&path)?;
56 serde_yaml::from_str(&raw).map_err(|e| Error::Config(e.to_string()))
57}
58
59pub fn resolve_profile(name: &str) -> Result<ResolvedProfile> {
61 let mut chain = Vec::new();
62 let mut current = name.to_string();
63 let max_depth = 10;
64
65 for _ in 0..max_depth {
66 if chain.contains(¤t) {
67 return Err(Error::Config(format!(
68 "Circular profile inheritance detected: {}",
69 chain.join(" → ")
70 )));
71 }
72 let profile = load_profile(¤t)?;
73 chain.push(current.clone());
74 if let Some(ref parent) = profile.extends {
75 current = parent.clone();
76 } else {
77 break;
78 }
79 }
80
81 let mut resolved = ResolvedProfile {
83 name: name.to_string(),
84 description: None,
85 pins: Vec::new(),
86 context: Vec::new(),
87 rules: Vec::new(),
88 };
89
90 for profile_name in chain.iter().rev() {
92 let profile = load_profile(profile_name)?;
93 if profile.description.is_some() {
94 resolved.description = profile.description;
95 }
96 for pin in &profile.pins {
98 if !resolved.pins.contains(pin) {
99 resolved.pins.push(pin.clone());
100 }
101 }
102 for ctx in &profile.context {
103 if !resolved.context.contains(ctx) {
104 resolved.context.push(ctx.clone());
105 }
106 }
107 for rule in &profile.rules {
108 if !resolved.rules.contains(rule) {
109 resolved.rules.push(rule.clone());
110 }
111 }
112 }
113
114 Ok(resolved)
115}
116
117pub fn list_profiles() -> Vec<(String, Option<String>)> {
119 let dir = match profiles_dir() {
120 Some(d) if d.exists() => d,
121 _ => return vec![],
122 };
123
124 let mut profiles = Vec::new();
125 let entries = match fs::read_dir(&dir) {
126 Ok(e) => e,
127 Err(_) => return vec![],
128 };
129
130 for entry in entries.filter_map(|e| e.ok()) {
131 let path = entry.path();
132 let ext = path.extension().and_then(|e| e.to_str());
133 if ext != Some("yaml") && ext != Some("yml") {
134 continue;
135 }
136 let stem = path
137 .file_stem()
138 .unwrap_or_default()
139 .to_string_lossy()
140 .to_string();
141 let desc = fs::read_to_string(&path)
142 .ok()
143 .and_then(|s| serde_yaml::from_str::<Profile>(&s).ok())
144 .and_then(|p| p.description);
145 profiles.push((stem, desc));
146 }
147
148 profiles.sort_by(|a, b| a.0.cmp(&b.0));
149 profiles
150}
151
152pub fn get_active_profile() -> Option<String> {
154 if let Ok(profile) = std::env::var("CHUB_PROFILE") {
156 if !profile.is_empty() {
157 return Some(profile);
158 }
159 }
160
161 let session_path = project_chub_dir()?.join(".active_profile");
163 fs::read_to_string(&session_path)
164 .ok()
165 .map(|s| s.trim().to_string())
166 .filter(|s| !s.is_empty())
167}
168
169pub fn set_active_profile(name: Option<&str>) -> Result<()> {
171 let chub_dir = project_chub_dir().ok_or_else(|| {
172 Error::Config("No .chub/ directory found. Run `chub init` first.".to_string())
173 })?;
174
175 let session_path = chub_dir.join(".active_profile");
176
177 match name {
178 Some(n) => {
179 let _ = load_profile(n)?;
181 fs::write(&session_path, n)?;
182 }
183 None => {
184 let _ = fs::remove_file(&session_path);
185 }
186 }
187
188 Ok(())
189}
190
191pub fn auto_detect_profile(file_path: &str) -> Option<String> {
193 let project_config = crate::team::project::load_project_config()?;
194 let auto_profiles = project_config.auto_profile?;
195
196 for entry in &auto_profiles {
197 let pattern = format!("**/{}", entry.path);
198 if let Ok(glob) = globset::Glob::new(&pattern) {
199 let matcher = glob.compile_matcher();
200 if matcher.is_match(file_path) {
201 return Some(entry.profile.clone());
202 }
203 }
204 let prefix = entry.path.trim_end_matches("**").trim_end_matches('/');
206 if file_path.starts_with(prefix) {
207 return Some(entry.profile.clone());
208 }
209 }
210
211 None
212}