lean_ctx/cli/dispatch/
mod.rs1use crate::{
2 core, doctor, heatmap, hook_handlers, report, setup, shell, status, token_report, tools,
3 uninstall,
4};
5
6mod analytics;
7mod help;
8mod lifecycle;
9mod network;
10mod server;
11
12#[allow(clippy::wildcard_imports)]
13use analytics::*;
14#[allow(clippy::wildcard_imports)]
15use help::*;
16#[allow(clippy::wildcard_imports)]
17use lifecycle::*;
18#[allow(clippy::wildcard_imports)]
19use network::*;
20#[allow(clippy::wildcard_imports)]
21use server::*;
22
23pub fn run() {
24 let mut args: Vec<String> = std::env::args().collect();
25
26 if args.get(1).is_some_and(|a| a == "(deleted)") {
30 args.remove(1);
31 }
32
33 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
34 if !enters_mcp {
35 crate::core::logging::init_logging();
36 }
37
38 if args.len() > 1 {
39 let rest = args[2..].to_vec();
40
41 match args[1].as_str() {
42 "-c" | "exec" => {
43 let raw = rest.first().is_some_and(|a| a == "--raw");
44 let cmd_args = if raw { &args[3..] } else { &args[2..] };
45 let command = if cmd_args.len() == 1 {
46 cmd_args[0].clone()
47 } else {
48 shell::join_command(cmd_args)
49 };
50 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
51 || std::env::var("LEAN_CTX_DISABLED").is_ok()
52 {
53 passthrough(&command);
54 }
55 if raw {
56 std::env::set_var("LEAN_CTX_RAW", "1");
57 } else {
58 std::env::set_var("LEAN_CTX_COMPRESS", "1");
59 }
60 let code = shell::exec(&command);
61 core::stats::flush();
62 core::heatmap::flush();
63 std::process::exit(code);
64 }
65 "-t" | "--track" => {
66 let cmd_args = &args[2..];
67 let code = if cmd_args.len() > 1 {
68 shell::exec_argv(cmd_args)
69 } else {
70 let command = cmd_args[0].clone();
71 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
72 || std::env::var("LEAN_CTX_DISABLED").is_ok()
73 {
74 passthrough(&command);
75 }
76 shell::exec(&command)
77 };
78 core::stats::flush();
79 core::heatmap::flush();
80 std::process::exit(code);
81 }
82 "shell" | "--shell" => {
83 shell::interactive();
84 return;
85 }
86 "gain" => {
87 cmd_gain(&rest);
88 return;
89 }
90 "token-report" | "report-tokens" => {
91 let code = token_report::run_cli(&rest);
92 if code != 0 {
93 std::process::exit(code);
94 }
95 return;
96 }
97 "pack" => {
98 crate::cli::cmd_pack(&rest);
99 return;
100 }
101 "proof" => {
102 crate::cli::cmd_proof(&rest);
103 return;
104 }
105 "verify" => {
106 crate::cli::cmd_verify(&rest);
107 return;
108 }
109 "audit" => {
110 println!("{}", crate::cli::audit_report::generate_report());
111 return;
112 }
113 "instructions" => {
114 crate::cli::cmd_instructions(&rest);
115 return;
116 }
117 "index" => {
118 crate::cli::cmd_index(&rest);
119 return;
120 }
121 "cep" => {
122 println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
123 return;
124 }
125 "dashboard" => {
126 cmd_dashboard(&rest);
127 return;
128 }
129 "team" => {
130 cmd_team(&rest);
131 return;
132 }
133 "provider" => {
134 cmd_provider(&rest);
135 return;
136 }
137 "serve" => {
138 cmd_serve(&rest);
139 return;
140 }
141 "watch" => {
142 cmd_watch(&rest);
143 return;
144 }
145 "proxy" => {
146 cmd_proxy(&rest);
147 return;
148 }
149 "daemon" => {
150 cmd_daemon(&rest);
151 return;
152 }
153 "init" => {
154 super::cmd_init(&rest);
155 return;
156 }
157 "setup" => {
158 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
159 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
160 let fix = rest.iter().any(|a| a == "--fix");
161 let json = rest.iter().any(|a| a == "--json");
162 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
163
164 if non_interactive || fix || json || yes {
165 let opts = setup::SetupOptions {
166 non_interactive,
167 yes,
168 fix,
169 json,
170 no_auto_approve,
171 ..Default::default()
172 };
173 match setup::run_setup_with_options(opts) {
174 Ok(report) => {
175 if json {
176 println!(
177 "{}",
178 serde_json::to_string_pretty(&report)
179 .unwrap_or_else(|_| "{}".to_string())
180 );
181 }
182 if !report.success {
183 std::process::exit(1);
184 }
185 }
186 Err(e) => {
187 eprintln!("{e}");
188 std::process::exit(1);
189 }
190 }
191 } else {
192 setup::run_setup();
193 }
194 return;
195 }
196 "install" => {
197 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
198 let json = rest.iter().any(|a| a == "--json");
199 if !repair {
200 eprintln!("Usage: lean-ctx install --repair [--json]");
201 std::process::exit(1);
202 }
203 let opts = setup::SetupOptions {
204 non_interactive: true,
205 yes: true,
206 fix: true,
207 json,
208 ..Default::default()
209 };
210 match setup::run_setup_with_options(opts) {
211 Ok(report) => {
212 if json {
213 println!(
214 "{}",
215 serde_json::to_string_pretty(&report)
216 .unwrap_or_else(|_| "{}".to_string())
217 );
218 }
219 if !report.success {
220 std::process::exit(1);
221 }
222 }
223 Err(e) => {
224 eprintln!("{e}");
225 std::process::exit(1);
226 }
227 }
228 return;
229 }
230 "bootstrap" => {
231 let json = rest.iter().any(|a| a == "--json");
232 let opts = setup::SetupOptions {
233 non_interactive: true,
234 yes: true,
235 fix: true,
236 json,
237 ..Default::default()
238 };
239 match setup::run_setup_with_options(opts) {
240 Ok(report) => {
241 if json {
242 println!(
243 "{}",
244 serde_json::to_string_pretty(&report)
245 .unwrap_or_else(|_| "{}".to_string())
246 );
247 }
248 if !report.success {
249 std::process::exit(1);
250 }
251 }
252 Err(e) => {
253 eprintln!("{e}");
254 std::process::exit(1);
255 }
256 }
257 return;
258 }
259 "status" => {
260 let code = status::run_cli(&rest);
261 if code != 0 {
262 std::process::exit(code);
263 }
264 return;
265 }
266 "read" => {
267 super::cmd_read(&rest);
268 core::stats::flush();
269 return;
270 }
271 "diff" => {
272 super::cmd_diff(&rest);
273 core::stats::flush();
274 return;
275 }
276 "grep" => {
277 super::cmd_grep(&rest);
278 core::stats::flush();
279 return;
280 }
281 "find" => {
282 super::cmd_find(&rest);
283 core::stats::flush();
284 return;
285 }
286 "ls" => {
287 super::cmd_ls(&rest);
288 core::stats::flush();
289 return;
290 }
291 "deps" => {
292 super::cmd_deps(&rest);
293 core::stats::flush();
294 return;
295 }
296 "discover" => {
297 super::cmd_discover(&rest);
298 return;
299 }
300 "ghost" => {
301 super::cmd_ghost(&rest);
302 return;
303 }
304 "filter" => {
305 super::cmd_filter(&rest);
306 return;
307 }
308 "heatmap" => {
309 heatmap::cmd_heatmap(&rest);
310 return;
311 }
312 "graph" => {
313 cmd_graph(&rest);
314 return;
315 }
316 "smells" => {
317 cmd_smells(&rest);
318 return;
319 }
320 "session" => {
321 super::cmd_session_action(&rest);
322 return;
323 }
324 "ledger" => {
325 super::cmd_ledger(&rest);
326 return;
327 }
328 "control" | "context-control" => {
329 super::cmd_control(&rest);
330 return;
331 }
332 "plan" | "context-plan" => {
333 super::cmd_plan(&rest);
334 return;
335 }
336 "compile" | "context-compile" => {
337 super::cmd_compile(&rest);
338 return;
339 }
340 "knowledge" => {
341 super::cmd_knowledge(&rest);
342 return;
343 }
344 "overview" => {
345 super::cmd_overview(&rest);
346 return;
347 }
348 "compress" => {
349 super::cmd_compress(&rest);
350 return;
351 }
352 "wrapped" => {
353 super::cmd_wrapped(&rest);
354 return;
355 }
356 "sessions" => {
357 super::cmd_sessions(&rest);
358 return;
359 }
360 "benchmark" => {
361 super::cmd_benchmark(&rest);
362 return;
363 }
364 "compact" => {
365 cmd_compact(&rest);
366 return;
367 }
368 "profile" => {
369 super::cmd_profile(&rest);
370 return;
371 }
372 "config" => {
373 super::cmd_config(&rest);
374 return;
375 }
376 "stats" => {
377 super::cmd_stats(&rest);
378 return;
379 }
380 "cache" => {
381 super::cmd_cache(&rest);
382 return;
383 }
384 "theme" => {
385 super::cmd_theme(&rest);
386 return;
387 }
388 "tee" => {
389 super::cmd_tee(&rest);
390 return;
391 }
392 "terse" | "compression" => {
393 super::cmd_compression(&rest);
394 return;
395 }
396 "slow-log" => {
397 super::cmd_slow_log(&rest);
398 return;
399 }
400 "update" | "--self-update" => {
401 core::updater::run(&rest);
402 return;
403 }
404 "restart" => {
405 cmd_restart();
406 return;
407 }
408 "stop" => {
409 cmd_stop();
410 return;
411 }
412 "dev-install" => {
413 cmd_dev_install();
414 return;
415 }
416 "doctor" => {
417 let code = doctor::run_cli(&rest);
418 if code != 0 {
419 std::process::exit(code);
420 }
421 return;
422 }
423 "harden" => {
424 super::harden::run(&rest);
425 return;
426 }
427 "export-rules" => {
428 super::export_rules::run(&rest);
429 return;
430 }
431 "gotchas" | "bugs" => {
432 super::cloud::cmd_gotchas(&rest);
433 return;
434 }
435 "learn" => {
436 super::cmd_learn(&rest);
437 return;
438 }
439 "buddy" | "pet" => {
440 super::cloud::cmd_buddy(&rest);
441 return;
442 }
443 "hook" => {
444 hook_handlers::mark_hook_environment();
445 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
446 let action = rest.first().map_or("help", std::string::String::as_str);
447 match action {
448 "rewrite" => hook_handlers::handle_rewrite(),
449 "redirect" => hook_handlers::handle_redirect(),
450 "observe" => hook_handlers::handle_observe(),
451 "copilot" => hook_handlers::handle_copilot(),
452 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
453 "codex-session-start" => hook_handlers::handle_codex_session_start(),
454 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
455 _ => {
456 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
457 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
458 std::process::exit(1);
459 }
460 }
461 return;
462 }
463 "report-issue" | "report" => {
464 report::run(&rest);
465 return;
466 }
467 "uninstall" => {
468 let dry_run = rest.iter().any(|a| a == "--dry-run");
469 let keep_config = rest.iter().any(|a| a == "--keep-config");
470 uninstall::run(dry_run, keep_config);
471 return;
472 }
473 "bypass" => {
474 if rest.is_empty() {
475 eprintln!("Usage: lean-ctx bypass \"command\"");
476 eprintln!("Runs the command with zero compression (raw passthrough).");
477 std::process::exit(1);
478 }
479 let command = if rest.len() == 1 {
480 rest[0].clone()
481 } else {
482 shell::join_command(&args[2..])
483 };
484 std::env::set_var("LEAN_CTX_RAW", "1");
485 let code = shell::exec(&command);
486 std::process::exit(code);
487 }
488 "safety-levels" | "safety" => {
489 println!("{}", core::compression_safety::format_safety_table());
490 return;
491 }
492 "cheat" | "cheatsheet" | "cheat-sheet" => {
493 super::cmd_cheatsheet();
494 return;
495 }
496 "login" => {
497 super::cloud::cmd_login(&rest);
498 return;
499 }
500 "register" => {
501 super::cloud::cmd_register(&rest);
502 return;
503 }
504 "forgot-password" => {
505 super::cloud::cmd_forgot_password(&rest);
506 return;
507 }
508 "sync" => {
509 super::cloud::cmd_sync();
510 return;
511 }
512 "contribute" => {
513 super::cloud::cmd_contribute();
514 return;
515 }
516 "cloud" => {
517 super::cloud::cmd_cloud(&rest);
518 return;
519 }
520 "upgrade" => {
521 super::cloud::cmd_upgrade();
522 return;
523 }
524 "--version" | "-V" => {
525 println!("{}", core::integrity::origin_line());
526 return;
527 }
528 "--help" | "-h" => {
529 print_help();
530 return;
531 }
532 "mcp" => {}
533 _ => {
534 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
535 print_help();
536 std::process::exit(1);
537 }
538 }
539 }
540
541 if args.len() == 1 && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
547 print_quickstart();
548 return;
549 }
550
551 if let Err(e) = run_mcp_server() {
552 tracing::error!("lean-ctx: {e}");
553 std::process::exit(1);
554 }
555}
556
557fn passthrough(command: &str) -> ! {
558 let (shell, flag) = shell::shell_and_flag();
559 let mut cmd = std::process::Command::new(&shell);
560 cmd.arg(&flag).arg(command).env("LEAN_CTX_ACTIVE", "1");
561 shell::platform::apply_utf8_locale(&mut cmd);
562 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
563 std::process::exit(status);
564}
565
566pub(super) fn run_async<F: std::future::Future>(future: F) -> F::Output {
567 tokio::runtime::Runtime::new()
568 .expect("failed to create async runtime")
569 .block_on(future)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use serial_test::serial;
576
577 #[test]
578 fn quickstart_is_short_and_points_to_setup() {
579 let q = quickstart_text();
580 assert!(
581 q.contains("lean-ctx setup"),
582 "quickstart must point to setup"
583 );
584 assert!(q.contains("--help"), "quickstart must point to full help");
585 assert!(
587 q.lines().count() <= 16,
588 "quickstart should be short; got {} lines",
589 q.lines().count()
590 );
591 assert!(
592 !q.contains("COMMANDS:"),
593 "quickstart must not inline the full command reference"
594 );
595 }
596
597 #[test]
598 fn capability_banner_tool_count_matches_registry() {
599 let n = crate::server::registry::tool_count();
600 let banner = capability_banner();
601 assert!(
602 banner.contains(&format!("{n} MCP tools")),
603 "banner must show the live registry count ({n}); got: {banner}"
604 );
605 }
606
607 #[test]
608 #[serial]
609 fn worker_threads_default_clamps_low() {
610 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
611 assert_eq!(resolve_worker_threads(1), 1);
612 }
613
614 #[test]
615 #[serial]
616 fn worker_threads_default_clamps_high() {
617 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
618 assert_eq!(resolve_worker_threads(32), 4);
619 }
620
621 #[test]
622 #[serial]
623 fn worker_threads_default_passthrough() {
624 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
625 assert_eq!(resolve_worker_threads(3), 3);
626 }
627
628 #[test]
629 #[serial]
630 fn worker_threads_env_override() {
631 std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
632 assert_eq!(resolve_worker_threads(2), 12);
633 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
634 }
635
636 #[test]
637 #[serial]
638 fn worker_threads_env_invalid_falls_back() {
639 std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
640 assert_eq!(resolve_worker_threads(3), 3);
641 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
642 }
643}