Skip to main content

lean_ctx/tools/
ctx_rules.rs

1use 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            // Drift is measured against the canonical rule source, so this needs
22            // no `.lean-ctx/rules.toml` and cannot error on a missing config (#548).
23            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}