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