Skip to main content

lean_ctx/cli/
rules_cmd.rs

1use crate::core::contextops::{self, ContextOps};
2
3pub fn cmd_rules(args: &[String]) {
4    let action = args.first().map_or("help", String::as_str);
5
6    let Some(home) = dirs::home_dir() else {
7        eprintln!("Error: could not determine home directory");
8        std::process::exit(1);
9    };
10
11    let project_root = std::env::current_dir().unwrap_or_else(|_| home.clone());
12    let ops = ContextOps::new(&home, &project_root);
13
14    match action {
15        "sync" => cmd_sync(&ops, args),
16        "diff" => cmd_diff(&ops),
17        "lint" => cmd_lint(&ops),
18        "status" => cmd_status(&ops),
19        "init" => cmd_init(&ops),
20        "dedup" => {
21            let apply = args.iter().any(|a| a == "--apply");
22            std::process::exit(crate::cli::rules_dedup::run(apply));
23        }
24        "help" | "--help" | "-h" => print_help(),
25        _ => {
26            eprintln!("Unknown rules action: {action}");
27            print_help();
28            std::process::exit(1);
29        }
30    }
31}
32
33fn cmd_sync(ops: &ContextOps, args: &[String]) {
34    let agent = args.get(1).map(String::as_str);
35
36    let report = if let Some(agent_name) = agent {
37        println!("Syncing rules for {agent_name}...");
38        ops.sync_agent(agent_name)
39    } else {
40        println!("Syncing rules to all detected agents...");
41        ops.sync_all()
42    };
43
44    println!("{}", contextops::format_sync(&report));
45
46    if !report.errors.is_empty() {
47        std::process::exit(1);
48    }
49}
50
51fn cmd_diff(ops: &ContextOps) {
52    match ops.detect_drift() {
53        Ok(reports) => {
54            println!("{}", contextops::format_drift(&reports));
55
56            let drifted = reports
57                .iter()
58                .filter(|r| r.status == contextops::DriftStatus::Drifted)
59                .count();
60            if drifted > 0 {
61                println!("\n{drifted} target(s) drifted. Run `lean-ctx rules sync` to fix.");
62            }
63        }
64        Err(e) => {
65            eprintln!("Error: {e}");
66            println!("\nFalling back to status-based drift check...");
67            let statuses = ops.status();
68            let outdated: Vec<_> = statuses.iter().filter(|s| s.state == "outdated").collect();
69            if outdated.is_empty() {
70                println!("All detected targets appear up to date.");
71            } else {
72                for s in &outdated {
73                    println!("  [OUTDATED] {} ({})", s.name, s.path);
74                }
75            }
76        }
77    }
78}
79
80fn cmd_lint(ops: &ContextOps) {
81    match ops.lint() {
82        Ok(warnings) => {
83            println!("{}", contextops::format_lint(&warnings));
84            let errors = warnings
85                .iter()
86                .filter(|w| w.severity == contextops::LintSeverity::Error)
87                .count();
88            if errors > 0 {
89                std::process::exit(1);
90            }
91        }
92        Err(e) => {
93            eprintln!("Error: {e}");
94            eprintln!("Run `lean-ctx rules init` first to create .lean-ctx/rules.toml");
95            std::process::exit(1);
96        }
97    }
98}
99
100fn cmd_status(ops: &ContextOps) {
101    let statuses = ops.status();
102    println!("{}", contextops::format_status(&statuses));
103
104    let has_config = ops.has_config();
105    println!();
106    if has_config {
107        println!("Central config: ✓ (.lean-ctx/rules.toml)");
108    } else {
109        println!("Central config: ✗ (run `lean-ctx rules init` to create)");
110    }
111}
112
113fn cmd_init(ops: &ContextOps) {
114    if ops.has_config() {
115        eprintln!("Config already exists at .lean-ctx/rules.toml");
116        eprintln!("Delete it first if you want to reinitialize.");
117        std::process::exit(1);
118    }
119
120    match ops.init() {
121        Ok(_config) => {
122            println!("Created .lean-ctx/rules.toml from existing rules.");
123            println!();
124            println!("Next steps:");
125            println!("  1. Review .lean-ctx/rules.toml");
126            println!("  2. Run `lean-ctx rules lint` to check consistency");
127            println!("  3. Run `lean-ctx rules sync` to distribute");
128        }
129        Err(e) => {
130            eprintln!("Error: {e}");
131            std::process::exit(1);
132        }
133    }
134}
135
136fn print_help() {
137    eprintln!(
138        "lean-ctx rules — Cross-agent rules governance (ContextOps)\n\
139         \n\
140         USAGE:\n    \
141             lean-ctx rules <action> [args]\n\
142         \n\
143         ACTIONS:\n    \
144             sync [agent]      Sync central rules to all (or one) agent config(s)\n    \
145             diff              Show drift between central and distributed rules\n    \
146             lint              Check rules for consistency and completeness\n    \
147             status            Show sync status for all targets\n    \
148             init              Create .lean-ctx/rules.toml from existing rules\n    \
149             dedup [--apply]   Remove duplicated lean-ctx rules (#578); dry-run by default\n    \
150             help              Show this help"
151    );
152}