lean_ctx/tools/
ctx_rules.rs1use crate::core::contextops::{self, ContextOps};
2
3pub fn handle(action: &str, agent: Option<&str>) -> String {
4 let Some(home) = dirs::home_dir() else {
5 return "Error: could not determine home directory".to_string();
6 };
7
8 let project_root = std::env::current_dir().unwrap_or_else(|_| home.clone());
9 let ops = ContextOps::new(&home, &project_root);
10
11 match action {
12 "sync" => {
13 let report = if let Some(agent_name) = agent {
14 ops.sync_agent(agent_name)
15 } else {
16 ops.sync_all()
17 };
18 contextops::format_sync(&report)
19 }
20 "diff" => {
21 let reports = ops.detect_drift();
24 let mut output = contextops::format_drift(&reports);
25 let drifted = reports
26 .iter()
27 .filter(|r| r.status == contextops::DriftStatus::Drifted)
28 .count();
29 if drifted > 0 {
30 output.push_str(&format!(
31 "\n\n{drifted} target(s) drifted. Run ctx_rules(action=\"sync\") to fix."
32 ));
33 }
34 output
35 }
36 "lint" => match ops.lint() {
37 Ok(warnings) => contextops::format_lint(&warnings),
38 Err(e) => {
39 format!("Error: {e}\nRun ctx_rules(action=\"init\") to create .lean-ctx/rules.toml")
40 }
41 },
42 "status" => {
43 let statuses = ops.status();
44 let mut output = contextops::format_status(&statuses);
45 output.push('\n');
46 if ops.has_config() {
47 output.push_str("\nCentral config: present (.lean-ctx/rules.toml)");
48 } else {
49 output.push_str(
50 "\nCentral config: missing (run ctx_rules(action=\"init\") to create)",
51 );
52 }
53 output
54 }
55 "init" => {
56 if ops.has_config() {
57 return "Config already exists at .lean-ctx/rules.toml. Delete it first to reinitialize.".to_string();
58 }
59 match ops.init() {
60 Ok(_) => "Created .lean-ctx/rules.toml from existing rules. It feeds ctx_rules(action=\"lint\") for cross-agent consistency and is a user-editable inventory; it is NOT the source for sync/diff, which (re)generate from lean-ctx's built-in canonical rules. Next: ctx_rules(action=\"lint\") to check, then ctx_rules(action=\"sync\") to (re)write the canonical block.".to_string(),
61 Err(e) => format!("Error: {e}"),
62 }
63 }
64 _ => "Unknown action. Use: sync, diff, lint, status, init".to_string(),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn handle_status() {
74 let result = handle("status", None);
75 assert!(result.contains("Agent Rules Status"));
76 }
77
78 #[test]
79 fn handle_unknown_action() {
80 let result = handle("unknown_xyz", None);
81 assert!(result.contains("Unknown action"));
82 }
83
84 #[test]
85 fn handle_lint_without_config() {
86 let result = handle("lint", None);
87 assert!(
88 result.contains("Error") || result.contains("Lint"),
89 "Should show error or lint results: {result}"
90 );
91 }
92
93 #[test]
94 fn handle_diff_without_config() {
95 let result = handle("diff", None);
96 assert!(!result.is_empty());
97 }
98}