1use crate::steering::{AgentContext, SteeringEngine, SteeringInclusion, SteeringRule};
6use crate::{DrivenError, Result};
7use std::path::{Path, PathBuf};
8
9pub struct SteerCommand;
11
12impl SteerCommand {
13 pub fn list(project_root: &Path) -> Result<Vec<SteeringInfo>> {
15 let steering_dir = project_root.join(".driven/steering");
16 let mut engine = SteeringEngine::with_steering_dir(&steering_dir);
17
18 if steering_dir.exists() {
19 engine.load_steering(&steering_dir)?;
20 }
21
22 let rules: Vec<SteeringInfo> = engine.list_rules().iter().map(SteeringInfo::from).collect();
23
24 Ok(rules)
25 }
26
27 pub fn add(
29 project_root: &Path,
30 id: &str,
31 name: Option<&str>,
32 inclusion_type: &str,
33 pattern_or_key: Option<&str>,
34 content: &str,
35 priority: Option<u8>,
36 ) -> Result<()> {
37 let steering_dir = project_root.join(".driven/steering");
38 std::fs::create_dir_all(&steering_dir).map_err(DrivenError::Io)?;
39
40 let inclusion = Self::parse_inclusion(inclusion_type, pattern_or_key)?;
42
43 let rule = SteeringRule::new(id)
45 .with_name(name.unwrap_or(id))
46 .with_inclusion(inclusion)
47 .with_content(content)
48 .with_priority(priority.unwrap_or(100));
49
50 let rule_path = steering_dir.join(format!("{}.md", id));
52
53 let engine = SteeringEngine::new();
54 engine.save_rule(&rule, &rule_path)?;
55
56 super::print_success(&format!("Steering rule '{}' added successfully", id));
57 Ok(())
58 }
59
60 pub fn remove(project_root: &Path, id: &str) -> Result<()> {
62 let steering_dir = project_root.join(".driven/steering");
63
64 if !steering_dir.exists() {
65 return Err(DrivenError::Config(
66 "No steering directory found".to_string(),
67 ));
68 }
69
70 let rule_path = steering_dir.join(format!("{}.md", id));
71
72 if rule_path.exists() {
73 std::fs::remove_file(&rule_path).map_err(DrivenError::Io)?;
74 super::print_success(&format!("Steering rule '{}' removed successfully", id));
75 Ok(())
76 } else {
77 Err(DrivenError::Config(format!(
78 "Steering rule with ID '{}' not found",
79 id
80 )))
81 }
82 }
83
84 pub fn test(
86 project_root: &Path,
87 file_path: &Path,
88 manual_keys: &[String],
89 ) -> Result<Vec<SteeringInfo>> {
90 let steering_dir = project_root.join(".driven/steering");
91 let mut engine = SteeringEngine::with_steering_dir(&steering_dir);
92
93 if steering_dir.exists() {
94 engine.load_steering(&steering_dir)?;
95 }
96
97 let mut context = AgentContext::new().with_file(file_path);
99 for key in manual_keys {
100 context = context.with_manual_key(key);
101 }
102
103 let rules = engine.get_rules_for_context(&context);
105
106 Ok(rules.iter().map(|r| SteeringInfo::from(*r)).collect())
107 }
108
109 pub fn show(project_root: &Path, id: &str) -> Result<SteeringInfo> {
111 let steering_dir = project_root.join(".driven/steering");
112 let mut engine = SteeringEngine::with_steering_dir(&steering_dir);
113
114 if steering_dir.exists() {
115 engine.load_steering(&steering_dir)?;
116 }
117
118 let rule = engine.get_rule(id).ok_or_else(|| {
119 DrivenError::Config(format!("Steering rule with ID '{}' not found", id))
120 })?;
121
122 Ok(SteeringInfo::from(rule))
123 }
124
125 pub fn inject(
127 project_root: &Path,
128 file_path: Option<&Path>,
129 manual_keys: &[String],
130 ) -> Result<String> {
131 let steering_dir = project_root.join(".driven/steering");
132 let mut engine = SteeringEngine::with_steering_dir(&steering_dir);
133
134 if steering_dir.exists() {
135 engine.load_steering(&steering_dir)?;
136 engine.resolve_all_file_references(project_root)?;
137 }
138
139 let mut context = AgentContext::new();
141 if let Some(path) = file_path {
142 context = context.with_file(path);
143 }
144 for key in manual_keys {
145 context = context.with_manual_key(key);
146 }
147
148 Ok(engine.inject_into_context(&context))
149 }
150
151 fn parse_inclusion(
153 inclusion_type: &str,
154 pattern_or_key: Option<&str>,
155 ) -> Result<SteeringInclusion> {
156 match inclusion_type.to_lowercase().as_str() {
157 "always" => Ok(SteeringInclusion::Always),
158 "filematch" | "file_match" | "file" => {
159 let pattern = pattern_or_key.ok_or_else(|| {
160 DrivenError::Config("fileMatch inclusion requires a pattern".to_string())
161 })?;
162 Ok(SteeringInclusion::FileMatch {
163 pattern: pattern.to_string(),
164 })
165 }
166 "manual" => {
167 let key = pattern_or_key.ok_or_else(|| {
168 DrivenError::Config("manual inclusion requires a key".to_string())
169 })?;
170 Ok(SteeringInclusion::Manual {
171 key: key.to_string(),
172 })
173 }
174 _ => Err(DrivenError::Config(format!(
175 "Unknown inclusion type: {}. Valid: always, fileMatch, manual",
176 inclusion_type
177 ))),
178 }
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct SteeringInfo {
185 pub id: String,
186 pub name: String,
187 pub inclusion_type: String,
188 pub inclusion_value: Option<String>,
189 pub priority: u8,
190 pub content_preview: String,
191 pub file_reference_count: usize,
192 pub source_path: Option<PathBuf>,
193}
194
195impl From<&SteeringRule> for SteeringInfo {
196 fn from(rule: &SteeringRule) -> Self {
197 let (inclusion_type, inclusion_value) = match &rule.inclusion {
198 SteeringInclusion::Always => ("always".to_string(), None),
199 SteeringInclusion::FileMatch { pattern } => {
200 ("fileMatch".to_string(), Some(pattern.clone()))
201 }
202 SteeringInclusion::Manual { key } => ("manual".to_string(), Some(key.clone())),
203 };
204
205 let content_preview = if rule.content.len() > 50 {
207 format!("{}...", &rule.content[..50].replace('\n', " "))
208 } else {
209 rule.content.replace('\n', " ")
210 };
211
212 Self {
213 id: rule.id.clone(),
214 name: rule.name.clone(),
215 inclusion_type,
216 inclusion_value,
217 priority: rule.priority,
218 content_preview,
219 file_reference_count: rule.file_references.len(),
220 source_path: rule.source_path.clone(),
221 }
222 }
223}
224
225pub fn print_steering_table(rules: &[SteeringInfo]) {
227 use console::style;
228
229 if rules.is_empty() {
230 println!("No steering rules configured.");
231 return;
232 }
233
234 println!(
236 "{:<20} {:<15} {:<25} {:<8}",
237 style("ID").bold(),
238 style("Inclusion").bold(),
239 style("Pattern/Key").bold(),
240 style("Priority").bold(),
241 );
242 println!("{}", "-".repeat(70));
243
244 for rule in rules {
246 let inclusion_value = rule.inclusion_value.as_deref().unwrap_or("-");
247
248 println!(
249 "{:<20} {:<15} {:<25} {:<8}",
250 rule.id, rule.inclusion_type, inclusion_value, rule.priority,
251 );
252 }
253}
254
255pub fn print_steering_details(rule: &SteeringInfo) {
257 use console::style;
258
259 println!("{}", style("Steering Rule Details").bold().underlined());
260 println!();
261 println!(" {}: {}", style("ID").bold(), rule.id);
262 println!(" {}: {}", style("Name").bold(), rule.name);
263 println!(
264 " {}: {}",
265 style("Inclusion Type").bold(),
266 rule.inclusion_type
267 );
268 if let Some(value) = &rule.inclusion_value {
269 println!(" {}: {}", style("Pattern/Key").bold(), value);
270 }
271 println!(" {}: {}", style("Priority").bold(), rule.priority);
272 println!(
273 " {}: {}",
274 style("File References").bold(),
275 rule.file_reference_count
276 );
277 if let Some(path) = &rule.source_path {
278 println!(" {}: {}", style("Source").bold(), path.display());
279 }
280 println!();
281 println!(
282 " {}: {}",
283 style("Content Preview").bold(),
284 rule.content_preview
285 );
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use tempfile::TempDir;
292
293 #[test]
294 fn test_parse_inclusion_always() {
295 let inclusion = SteerCommand::parse_inclusion("always", None).unwrap();
296 assert_eq!(inclusion, SteeringInclusion::Always);
297 }
298
299 #[test]
300 fn test_parse_inclusion_file_match() {
301 let inclusion = SteerCommand::parse_inclusion("fileMatch", Some("**/*.rs")).unwrap();
302 match inclusion {
303 SteeringInclusion::FileMatch { pattern } => {
304 assert_eq!(pattern, "**/*.rs");
305 }
306 _ => panic!("Wrong inclusion type"),
307 }
308 }
309
310 #[test]
311 fn test_parse_inclusion_manual() {
312 let inclusion = SteerCommand::parse_inclusion("manual", Some("rust-style")).unwrap();
313 match inclusion {
314 SteeringInclusion::Manual { key } => {
315 assert_eq!(key, "rust-style");
316 }
317 _ => panic!("Wrong inclusion type"),
318 }
319 }
320
321 #[test]
322 fn test_parse_inclusion_file_match_requires_pattern() {
323 let result = SteerCommand::parse_inclusion("fileMatch", None);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_parse_inclusion_manual_requires_key() {
329 let result = SteerCommand::parse_inclusion("manual", None);
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn test_add_and_list_steering() {
335 let temp_dir = TempDir::new().unwrap();
336 let project_root = temp_dir.path();
337
338 SteerCommand::add(
340 project_root,
341 "rust-standards",
342 Some("Rust Standards"),
343 "always",
344 None,
345 "# Rust Standards\n\nUse these standards.",
346 Some(50),
347 )
348 .unwrap();
349
350 let rules = SteerCommand::list(project_root).unwrap();
352 assert_eq!(rules.len(), 1);
353 assert_eq!(rules[0].id, "rust-standards");
354 assert_eq!(rules[0].name, "Rust Standards");
355 assert_eq!(rules[0].inclusion_type, "always");
356 assert_eq!(rules[0].priority, 50);
357 }
358
359 #[test]
360 fn test_remove_steering() {
361 let temp_dir = TempDir::new().unwrap();
362 let project_root = temp_dir.path();
363
364 SteerCommand::add(
366 project_root,
367 "test-rule",
368 None,
369 "always",
370 None,
371 "# Test",
372 None,
373 )
374 .unwrap();
375
376 SteerCommand::remove(project_root, "test-rule").unwrap();
378
379 let rules = SteerCommand::list(project_root).unwrap();
381 assert!(rules.is_empty());
382 }
383
384 #[test]
385 fn test_test_steering() {
386 let temp_dir = TempDir::new().unwrap();
387 let project_root = temp_dir.path();
388
389 SteerCommand::add(
391 project_root,
392 "always-rule",
393 None,
394 "always",
395 None,
396 "# Always",
397 None,
398 )
399 .unwrap();
400
401 SteerCommand::add(
403 project_root,
404 "rust-rule",
405 None,
406 "fileMatch",
407 Some("**/*.rs"),
408 "# Rust",
409 None,
410 )
411 .unwrap();
412
413 let rules = SteerCommand::test(project_root, Path::new("src/main.rs"), &[]).unwrap();
415 assert_eq!(rules.len(), 2); let rules = SteerCommand::test(project_root, Path::new("src/main.py"), &[]).unwrap();
419 assert_eq!(rules.len(), 1); }
421
422 #[test]
423 fn test_inject_steering() {
424 let temp_dir = TempDir::new().unwrap();
425 let project_root = temp_dir.path();
426
427 SteerCommand::add(
429 project_root,
430 "test-rule",
431 None,
432 "always",
433 None,
434 "# Test Content",
435 None,
436 )
437 .unwrap();
438
439 let content = SteerCommand::inject(project_root, None, &[]).unwrap();
441 assert!(content.contains("# Test Content"));
442 }
443}