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    // Drift is measured against the canonical rule source, so this never needs
53    // `.lean-ctx/rules.toml` (and never errors on a missing config) — see #548.
54    let reports = ops.detect_drift();
55    println!("{}", contextops::format_drift(&reports));
56
57    let drifted = reports
58        .iter()
59        .filter(|r| r.status == contextops::DriftStatus::Drifted)
60        .count();
61    if drifted > 0 {
62        println!("\n{drifted} target(s) drifted. Run `lean-ctx rules sync` to fix.");
63    }
64}
65
66fn cmd_lint(ops: &ContextOps) {
67    match ops.lint() {
68        Ok(warnings) => {
69            println!("{}", contextops::format_lint(&warnings));
70            let errors = warnings
71                .iter()
72                .filter(|w| w.severity == contextops::LintSeverity::Error)
73                .count();
74            if errors > 0 {
75                std::process::exit(1);
76            }
77        }
78        Err(e) => {
79            eprintln!("Error: {e}");
80            eprintln!("Run `lean-ctx rules init` first to create .lean-ctx/rules.toml");
81            std::process::exit(1);
82        }
83    }
84}
85
86fn cmd_status(ops: &ContextOps) {
87    let statuses = ops.status();
88    println!("{}", contextops::format_status(&statuses));
89
90    let has_config = ops.has_config();
91    println!();
92    if has_config {
93        println!("Central config: ✓ (.lean-ctx/rules.toml)");
94    } else {
95        println!("Central config: ✗ (run `lean-ctx rules init` to create)");
96    }
97}
98
99fn cmd_init(ops: &ContextOps) {
100    if ops.has_config() {
101        eprintln!("Config already exists at .lean-ctx/rules.toml");
102        eprintln!("Delete it first if you want to reinitialize.");
103        std::process::exit(1);
104    }
105
106    match ops.init() {
107        Ok(_config) => {
108            println!("Created .lean-ctx/rules.toml from existing rules.");
109            println!();
110            println!("Note: rules.toml is consumed by `lean-ctx rules lint` (cross-agent");
111            println!("consistency) and is a user-editable inventory. It is NOT the source");
112            println!("for `rules sync`/`diff` — those (re)generate from lean-ctx's built-in");
113            println!("canonical rules and preserve your own text around the markers.");
114            println!();
115            println!("Next steps:");
116            println!("  1. Review .lean-ctx/rules.toml");
117            println!("  2. Run `lean-ctx rules lint` to check consistency");
118            println!("  3. Run `lean-ctx rules sync` to (re)write the canonical rules block");
119        }
120        Err(e) => {
121            eprintln!("Error: {e}");
122            std::process::exit(1);
123        }
124    }
125}
126
127fn print_help() {
128    eprintln!(
129        "lean-ctx rules — Cross-agent rules governance (ContextOps)\n\
130         \n\
131         USAGE:\n    \
132             lean-ctx rules <action> [args]\n\
133         \n\
134         ACTIONS:\n    \
135             sync [agent]      (Re)write the canonical lean-ctx rules block into all (or one) agent config(s)\n    \
136             diff              Show drift between the canonical rules and each agent's on-disk block\n    \
137             lint              Check .lean-ctx/rules.toml for consistency and completeness\n    \
138             status            Show sync status for all targets\n    \
139             init              Create .lean-ctx/rules.toml from existing rules\n    \
140             dedup [--apply]   Remove duplicated lean-ctx rules (#578); dry-run by default\n    \
141             help              Show this help\n\
142         \n\
143         NOTES:\n    \
144             `sync` and `diff` use lean-ctx's built-in canonical rules as the source\n    \
145             of truth and preserve your own text around the `<!-- lean-ctx-rules -->`\n    \
146             markers. They do NOT read `.lean-ctx/rules.toml` — that file is the input\n    \
147             for `lint` and a user-editable inventory created by `init`."
148    );
149}