chub_core/team/
agent_config.rs1use std::fs;
2
3use crate::error::{Error, Result};
4use crate::team::context::list_context_docs;
5use crate::team::pins::list_pins;
6use crate::team::project::AgentRules;
7
8#[derive(Debug, Clone)]
10pub enum Target {
11 ClaudeMd,
12 CursorRules,
13 WindsurfRules,
14 AgentsMd,
15 Copilot,
16}
17
18impl Target {
19 pub fn parse_target(s: &str) -> Option<Self> {
20 match s.to_lowercase().as_str() {
21 "claude.md" | "claudemd" => Some(Target::ClaudeMd),
22 "cursorrules" | ".cursorrules" => Some(Target::CursorRules),
23 "windsurfrules" | ".windsurfrules" => Some(Target::WindsurfRules),
24 "agents.md" | "agentsmd" => Some(Target::AgentsMd),
25 "copilot" | "copilot-instructions" => Some(Target::Copilot),
26 _ => None,
27 }
28 }
29
30 pub fn filename(&self) -> &'static str {
31 match self {
32 Target::ClaudeMd => "CLAUDE.md",
33 Target::CursorRules => ".cursorrules",
34 Target::WindsurfRules => ".windsurfrules",
35 Target::AgentsMd => "AGENTS.md",
36 Target::Copilot => ".github/copilot-instructions.md",
37 }
38 }
39}
40
41pub fn load_agent_rules() -> Option<AgentRules> {
43 let config = crate::team::project::load_project_config()?;
44 config.agent_rules
45}
46
47pub fn generate_config(rules: &AgentRules) -> String {
49 let mut output = String::new();
50
51 output.push_str("# Project Rules\n\n");
53
54 if !rules.global.is_empty() {
56 for rule in &rules.global {
57 output.push_str(&format!("- {}\n", rule));
58 }
59 output.push('\n');
60 }
61
62 if rules.include_pins {
64 let pins = list_pins();
65 if !pins.is_empty() {
66 output.push_str("## Pinned Documentation\n");
67 output.push_str(
68 "Use `chub get <id>` to fetch these docs when working with these libraries:\n",
69 );
70 for pin in &pins {
71 let mut desc = format!("- {}", pin.id);
72 if let Some(ref lang) = pin.lang {
73 desc.push_str(&format!(" ({})", lang));
74 }
75 if let Some(ref version) = pin.version {
76 desc.push_str(&format!(" v{}", version));
77 }
78 if let Some(ref reason) = pin.reason {
79 desc.push_str(&format!(" — {}", reason));
80 }
81 output.push_str(&desc);
82 output.push('\n');
83 }
84 output.push('\n');
85 }
86 }
87
88 if rules.include_context {
90 let context_docs = list_context_docs();
91 if !context_docs.is_empty() {
92 output.push_str("## Project Context\n");
93 output.push_str("Use `chub get project/<name>` or ask Chub for these:\n");
94 for doc in &context_docs {
95 let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
96 let mut desc = format!("- project/{}", stem);
97 if !doc.description.is_empty() {
98 desc.push_str(&format!(" — {}", doc.description));
99 }
100 output.push_str(&desc);
101 output.push('\n');
102 }
103 output.push('\n');
104 }
105 }
106
107 for (module_name, module_rules) in &rules.modules {
109 output.push_str(&format!(
110 "## Module: {} ({})\n",
111 module_name, module_rules.path
112 ));
113 for rule in &module_rules.rules {
114 output.push_str(&format!("- {}\n", rule));
115 }
116 output.push('\n');
117 }
118
119 output
120}
121
122#[derive(Debug, Clone)]
124pub struct SyncResult {
125 pub target: String,
126 pub filename: String,
127 pub action: SyncAction,
128}
129
130#[derive(Debug, Clone)]
131pub enum SyncAction {
132 Created,
133 Updated,
134 Unchanged,
135}
136
137pub fn sync_configs() -> Result<Vec<SyncResult>> {
139 let rules = load_agent_rules().ok_or_else(|| {
140 Error::Config(
141 "No agent_rules found in .chub/config.yaml. Add agent_rules section first.".to_string(),
142 )
143 })?;
144
145 let project_root = crate::team::project::find_project_root(None).ok_or_else(|| {
146 Error::Config("No .chub/ directory found. Run `chub init` first.".to_string())
147 })?;
148
149 let content = generate_config(&rules);
150 let mut results = Vec::new();
151
152 for target_name in &rules.targets {
153 let target = match Target::parse_target(target_name) {
154 Some(t) => t,
155 None => continue,
156 };
157
158 let path = project_root.join(target.filename());
159
160 if let Some(parent) = path.parent() {
162 let _ = fs::create_dir_all(parent);
163 }
164
165 let action = if path.exists() {
166 let existing = fs::read_to_string(&path).unwrap_or_default();
167 if existing == content {
168 SyncAction::Unchanged
169 } else {
170 fs::write(&path, &content)?;
171 SyncAction::Updated
172 }
173 } else {
174 fs::write(&path, &content)?;
175 SyncAction::Created
176 };
177
178 results.push(SyncResult {
179 target: target_name.clone(),
180 filename: target.filename().to_string(),
181 action,
182 });
183 }
184
185 Ok(results)
186}
187
188pub fn diff_configs() -> Result<Vec<(String, String, Option<String>)>> {
190 let rules = load_agent_rules()
191 .ok_or_else(|| Error::Config("No agent_rules found in .chub/config.yaml.".to_string()))?;
192
193 let project_root = crate::team::project::find_project_root(None)
194 .ok_or_else(|| Error::Config("No .chub/ directory found.".to_string()))?;
195
196 let content = generate_config(&rules);
197 let mut diffs = Vec::new();
198
199 for target_name in &rules.targets {
200 let target = match Target::parse_target(target_name) {
201 Some(t) => t,
202 None => continue,
203 };
204
205 let path = project_root.join(target.filename());
206 let existing = if path.exists() {
207 Some(fs::read_to_string(&path).unwrap_or_default())
208 } else {
209 None
210 };
211
212 diffs.push((target.filename().to_string(), content.clone(), existing));
213 }
214
215 Ok(diffs)
216}