lean_ctx/cli/
rules_cmd.rs1use 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 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}