1use crate::{
2 core, doctor, heatmap, hook_handlers, report, setup, shell, status, token_report, uninstall,
3};
4
5mod analytics;
6mod help;
7mod lifecycle;
8mod network;
9mod server;
10
11#[allow(clippy::wildcard_imports)]
12use analytics::*;
13#[allow(clippy::wildcard_imports)]
14use help::*;
15#[allow(clippy::wildcard_imports)]
16use lifecycle::*;
17#[allow(clippy::wildcard_imports)]
18use network::*;
19#[allow(clippy::wildcard_imports)]
20use server::*;
21
22pub fn run() {
23 let mut args: Vec<String> = std::env::args().collect();
24
25 if args.get(1).is_some_and(|a| a == "(deleted)") {
29 args.remove(1);
30 }
31
32 if !is_server_mode(&args) {
33 restore_sigpipe_default();
34 }
35
36 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
37 if !enters_mcp {
38 crate::core::logging::init_logging();
39 }
40
41 if args.len() > 1 {
42 let rest = args[2..].to_vec();
43
44 match args[1].as_str() {
45 "-c" | "exec" => {
46 let raw = rest.first().is_some_and(|a| a == "--raw");
47 let cmd_args = if raw { &args[3..] } else { &args[2..] };
48 let command = if cmd_args.len() == 1 {
49 cmd_args[0].clone()
50 } else {
51 shell::join_command(cmd_args)
52 };
53 core::agent_runtime_env::capture();
57 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
58 || std::env::var("LEAN_CTX_DISABLED").is_ok()
59 {
60 passthrough(&command);
61 }
62 if raw {
63 std::env::set_var("LEAN_CTX_RAW", "1");
64 } else {
65 std::env::set_var("LEAN_CTX_COMPRESS", "1");
66 }
67 let code = shell::exec(&command);
68 core::stats::flush();
69 core::heatmap::flush();
70 core::path_mode_memory::flush();
71 core::auto_mode_resolver::flush_sources();
72 std::process::exit(code);
73 }
74 "-t" | "--track" => {
75 let cmd_args = &args[2..];
76 let code = if cmd_args.len() > 1 {
77 shell::exec_argv(cmd_args)
78 } else {
79 let command = cmd_args[0].clone();
80 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
81 || std::env::var("LEAN_CTX_DISABLED").is_ok()
82 {
83 passthrough(&command);
84 }
85 shell::exec(&command)
86 };
87 core::stats::flush();
88 core::heatmap::flush();
89 core::path_mode_memory::flush();
90 core::auto_mode_resolver::flush_sources();
91 std::process::exit(code);
92 }
93 "shell" | "--shell" => {
94 shell::interactive();
95 return;
96 }
97 "gain" => {
98 cmd_gain(&rest);
99 return;
100 }
101 "savings" => {
102 cmd_savings(&rest);
103 return;
104 }
105 "learning" => {
106 cmd_learning(&rest);
107 return;
108 }
109 "conformance" | "selftest" => {
110 cmd_conformance(&rest);
111 return;
112 }
113 "billing" => {
114 cmd_billing(&rest);
115 return;
116 }
117 "finops" => {
118 cmd_finops(&rest);
119 return;
120 }
121 "roi" => {
122 super::cmd_roi(&rest);
125 return;
126 }
127 "token-report" | "report-tokens" => {
128 let code = token_report::run_cli(&rest);
129 if code != 0 {
130 std::process::exit(code);
131 }
132 return;
133 }
134 "pack" => {
135 crate::cli::cmd_pack(&rest);
136 return;
137 }
138 "policy" => {
139 crate::cli::cmd_policy(&rest);
140 return;
141 }
142 "plugin" | "plugins" => {
143 crate::cli::plugin_cmd::cmd_plugin(&rest);
144 return;
145 }
146 "rules" => {
147 crate::cli::rules_cmd::cmd_rules(&rest);
148 return;
149 }
150 "proof" => {
151 crate::cli::cmd_proof(&rest);
152 return;
153 }
154 "verify" => {
155 crate::cli::cmd_verify(&rest);
156 return;
157 }
158 "eval" => {
159 crate::cli::eval_cmd::cmd_eval(&rest);
160 return;
161 }
162 "verify-cache" | "cache-selftest" => {
163 let code = crate::cli::verify_cache_cmd::cmd_verify_cache(&rest);
164 if code != 0 {
165 std::process::exit(code);
166 }
167 return;
168 }
169 "visualize" => {
170 super::cmd_visualize(&rest);
171 return;
172 }
173 "audit" => {
174 if rest.first().map(String::as_str) == Some("evidence") {
175 crate::cli::audit_report::cmd_evidence(&rest[1..]);
176 } else {
177 println!("{}", crate::cli::audit_report::generate_report());
178 }
179 return;
180 }
181 "agent" => {
182 crate::cli::cmd_agent(&rest);
183 return;
184 }
185 "instructions" => {
186 crate::cli::cmd_instructions(&rest);
187 return;
188 }
189 "index" => {
190 crate::cli::cmd_index(&rest);
191 return;
192 }
193 "semantic-search" | "search-code" => {
194 crate::cli::cmd_semantic_search(&rest);
195 core::stats::flush();
196 return;
197 }
198 "repomap" | "repo-map" => {
199 crate::cli::cmd_repomap(&rest);
200 core::stats::flush();
201 return;
202 }
203 "cep" => {
204 println!("{}", core::stats::format_cep_report());
205 return;
206 }
207 "dashboard" => {
208 cmd_dashboard(&rest);
209 return;
210 }
211 "team" => {
212 cmd_team(&rest);
213 return;
214 }
215 "provider" => {
216 cmd_provider(&rest);
217 return;
218 }
219 "serve" => {
220 cmd_serve(&rest);
221 return;
222 }
223 "watch" => {
224 cmd_watch(&rest);
225 return;
226 }
227 "proxy" => {
228 cmd_proxy(&rest);
229 return;
230 }
231 "daemon" => {
232 cmd_daemon(&rest);
233 return;
234 }
235 "init" => {
236 super::cmd_init(&rest);
237 return;
238 }
239 "setup" => {
240 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
241 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
242 let fix = rest.iter().any(|a| a == "--fix");
243 let json = rest.iter().any(|a| a == "--json");
244 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
245 let skip_rules = rest.iter().any(|a| a == "--skip-rules");
246
247 if non_interactive || fix || json || yes {
248 let opts = setup::SetupOptions {
249 non_interactive,
250 yes,
251 fix,
252 json,
253 no_auto_approve,
254 skip_rules,
255 ..Default::default()
256 };
257 match setup::run_setup_with_options(opts) {
258 Ok(report) => {
259 if json {
260 println!(
261 "{}",
262 serde_json::to_string_pretty(&report)
263 .unwrap_or_else(|_| "{}".to_string())
264 );
265 }
266 if !report.success {
267 std::process::exit(1);
268 }
269 }
270 Err(e) => {
271 eprintln!("{e}");
272 std::process::exit(1);
273 }
274 }
275 } else {
276 setup::run_setup();
277 }
278 return;
279 }
280 "onboard" => {
281 setup::run_onboard();
282 return;
283 }
284 "install" => {
285 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
290 let json = rest.iter().any(|a| a == "--json");
291 if !repair {
292 setup::run_setup();
293 return;
294 }
295 let opts = setup::SetupOptions {
296 non_interactive: true,
297 yes: true,
298 fix: true,
299 json,
300 ..Default::default()
301 };
302 match setup::run_setup_with_options(opts) {
303 Ok(report) => {
304 if json {
305 println!(
306 "{}",
307 serde_json::to_string_pretty(&report)
308 .unwrap_or_else(|_| "{}".to_string())
309 );
310 }
311 if !report.success {
312 std::process::exit(1);
313 }
314 }
315 Err(e) => {
316 eprintln!("{e}");
317 std::process::exit(1);
318 }
319 }
320 return;
321 }
322 "bootstrap" => {
323 let json = rest.iter().any(|a| a == "--json");
324 let opts = setup::SetupOptions {
325 non_interactive: true,
326 yes: true,
327 fix: true,
328 json,
329 ..Default::default()
330 };
331 match setup::run_setup_with_options(opts) {
332 Ok(report) => {
333 if json {
334 println!(
335 "{}",
336 serde_json::to_string_pretty(&report)
337 .unwrap_or_else(|_| "{}".to_string())
338 );
339 }
340 if !report.success {
341 std::process::exit(1);
342 }
343 }
344 Err(e) => {
345 eprintln!("{e}");
346 std::process::exit(1);
347 }
348 }
349 return;
350 }
351 "status" => {
352 let code = status::run_cli(&rest);
353 if code != 0 {
354 std::process::exit(code);
355 }
356 return;
357 }
358 "read" => {
359 super::cmd_read(&rest);
360 core::stats::flush();
361 return;
362 }
363 "call" => {
364 super::cmd_call(&rest);
365 return;
366 }
367 "diff" => {
368 super::cmd_diff(&rest);
369 core::stats::flush();
370 return;
371 }
372 "grep" => {
373 super::cmd_grep(&rest);
374 core::stats::flush();
375 return;
376 }
377 "find" => {
378 super::cmd_find(&rest);
379 core::stats::flush();
380 return;
381 }
382 "ls" => {
383 super::cmd_ls(&rest);
384 core::stats::flush();
385 return;
386 }
387 "deps" => {
388 super::cmd_deps(&rest);
389 core::stats::flush();
390 return;
391 }
392 "discover" => {
393 super::cmd_discover(&rest);
394 return;
395 }
396 "ghost" => {
397 super::cmd_ghost(&rest);
398 return;
399 }
400 "filter" => {
401 super::cmd_filter(&rest);
402 return;
403 }
404 "heatmap" => {
405 heatmap::cmd_heatmap(&rest);
406 return;
407 }
408 "graph" => {
409 cmd_graph(&rest);
410 return;
411 }
412 "smells" => {
413 cmd_smells(&rest);
414 return;
415 }
416 "session" => {
417 super::cmd_session_action(&rest);
418 return;
419 }
420 "ledger" => {
421 super::cmd_ledger(&rest);
422 return;
423 }
424 "control" | "context-control" => {
425 super::cmd_control(&rest);
426 return;
427 }
428 "plan" | "context-plan" => {
429 super::cmd_plan(&rest);
430 return;
431 }
432 "compile" | "context-compile" => {
433 super::cmd_compile(&rest);
434 return;
435 }
436 "knowledge" => {
437 super::cmd_knowledge(&rest);
438 return;
439 }
440 "skillify" => {
441 super::cmd_skillify(&rest);
442 return;
443 }
444 "summary" => {
445 super::cmd_summary(&rest);
446 return;
447 }
448 "overview" => {
449 super::cmd_overview(&rest);
450 return;
451 }
452 "compress" => {
453 super::cmd_compress(&rest);
454 return;
455 }
456 "wrapped" => {
457 eprintln!("'lean-ctx wrapped' has been removed. Use: lean-ctx gain --wrapped");
458 std::process::exit(1);
459 }
460 "sessions" | "session-store" => {
461 super::cmd_sessions(&rest);
462 return;
463 }
464 "benchmark" => {
465 super::cmd_benchmark(&rest);
466 return;
467 }
468 "compact" => {
469 cmd_compact(&rest);
470 return;
471 }
472 "profile" => {
473 super::cmd_profile(&rest);
474 return;
475 }
476 "tools" => {
477 let mut forwarded = vec!["tools".to_string()];
481 forwarded.extend(rest.iter().cloned());
482 super::cmd_profile(&forwarded);
483 return;
484 }
485 "config" => {
486 super::cmd_config(&rest);
487 return;
488 }
489 "allow" => {
490 super::cmd_allow(&rest);
491 return;
492 }
493 "stats" => {
494 super::cmd_stats(&rest);
495 return;
496 }
497 "cache" => {
498 super::cmd_cache(&rest);
499 return;
500 }
501 "theme" => {
502 super::cmd_theme(&rest);
503 return;
504 }
505 "tee" => {
506 super::cmd_tee(&rest);
507 return;
508 }
509 "terse" | "compression" => {
510 super::cmd_compression(&rest);
511 return;
512 }
513 "slow-log" => {
514 super::cmd_slow_log(&rest);
515 return;
516 }
517 "editor-signal" => {
520 let file = rest
521 .iter()
522 .position(|a| a == "--file")
523 .and_then(|i| rest.get(i + 1));
524 if let Some(path) = file {
525 if let Err(e) = core::editor_signal::record_focus(path) {
526 eprintln!("editor-signal: {e}");
527 std::process::exit(1);
528 }
529 } else {
530 eprintln!("usage: lean-ctx editor-signal --file <path>");
531 std::process::exit(2);
532 }
533 return;
534 }
535 "update" | "--self-update" => {
536 core::updater::run(&rest);
537 return;
538 }
539 "restart" => {
540 cmd_restart();
541 return;
542 }
543 "stop" => {
544 cmd_stop();
545 return;
546 }
547 "dev-install" => {
548 cmd_dev_install();
549 return;
550 }
551 "codesign-setup" => {
552 cmd_codesign_setup();
553 return;
554 }
555 "doctor" => {
556 let code = doctor::run_cli(&rest);
557 if code != 0 {
558 std::process::exit(code);
559 }
560 return;
561 }
562 "harden" => {
563 super::harden::run(&rest);
564 return;
565 }
566 "export-rules" => {
567 super::export_rules::run(&rest);
568 return;
569 }
570 "gotchas" | "bugs" => {
571 super::cloud::cmd_gotchas(&rest);
572 return;
573 }
574 "learn" => {
575 super::cmd_learn(&rest);
576 return;
577 }
578 "buddy" | "pet" => {
579 super::cloud::cmd_buddy(&rest);
580 return;
581 }
582 "hook" => {
583 hook_handlers::mark_hook_environment();
584 core::agent_runtime_env::capture();
588 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
589 let action = rest.first().map_or("help", std::string::String::as_str);
590 match action {
591 "rewrite" => hook_handlers::handle_rewrite(),
592 "redirect" => hook_handlers::handle_redirect(),
593 "observe" => hook_handlers::handle_observe(),
594 "copilot" => hook_handlers::handle_copilot(),
595 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
596 "codex-session-start" => hook_handlers::handle_codex_session_start(),
597 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
598 _ => {
599 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
600 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
601 std::process::exit(1);
602 }
603 }
604 return;
605 }
606 "report-issue" | "report" => {
607 report::run(&rest);
608 return;
609 }
610 "uninstall" => {
611 let dry_run = rest.iter().any(|a| a == "--dry-run");
612 let keep_config = rest.iter().any(|a| a == "--keep-config");
613 let keep_binary = rest.iter().any(|a| a == "--keep-binary");
614 uninstall::run(dry_run, keep_config, keep_binary);
615 return;
616 }
617 "bypass" => {
618 if rest.is_empty() {
619 eprintln!("Usage: lean-ctx bypass \"command\"");
620 eprintln!("Runs the command with zero compression (raw passthrough).");
621 std::process::exit(1);
622 }
623 let command = if rest.len() == 1 {
624 rest[0].clone()
625 } else {
626 shell::join_command(&args[2..])
627 };
628 std::env::set_var("LEAN_CTX_RAW", "1");
629 let code = shell::exec(&command);
630 std::process::exit(code);
631 }
632 "safety-levels" | "safety" => {
633 println!("{}", core::compression_safety::format_safety_table());
634 return;
635 }
636 "cheat" | "cheatsheet" | "cheat-sheet" => {
637 super::cmd_cheatsheet();
638 return;
639 }
640 "login" => {
641 super::cloud::cmd_login(&rest);
642 return;
643 }
644 "register" => {
645 super::cloud::cmd_register(&rest);
646 return;
647 }
648 "forgot-password" => {
649 super::cloud::cmd_forgot_password(&rest);
650 return;
651 }
652 "sync" => {
653 super::cloud::cmd_sync(&rest);
654 return;
655 }
656 "contribute" => {
657 super::cloud::cmd_contribute();
658 return;
659 }
660 "cloud" => {
661 super::cloud::cmd_cloud(&rest);
662 return;
663 }
664 "upgrade" => {
665 super::cloud::cmd_upgrade();
666 return;
667 }
668 "--version" | "-V" => {
669 println!("{}", core::integrity::origin_line());
670 return;
671 }
672 "help" => {
673 let want_all = rest
674 .iter()
675 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"));
676 if want_all {
677 print_help();
678 } else {
679 print_help_concise();
680 }
681 return;
682 }
683 "--help" | "-h" => {
684 if rest
685 .iter()
686 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"))
687 {
688 print_help();
689 } else {
690 print_help_concise();
691 }
692 return;
693 }
694 "mcp" => {}
695 _ => {
696 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
697 print_help_concise();
698 std::process::exit(1);
699 }
700 }
701 }
702
703 if args.len() == 1 && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
709 print_quickstart();
710 return;
711 }
712
713 if let Err(e) = run_mcp_server() {
714 tracing::error!("lean-ctx: {e}");
715 std::process::exit(1);
716 }
717}
718
719fn is_server_mode(args: &[String]) -> bool {
723 args.len() == 1
724 || args.get(1).is_some_and(|a| {
725 matches!(
726 a.as_str(),
727 "mcp" | "daemon" | "proxy" | "serve" | "watch" | "dashboard"
728 )
729 })
730}
731
732#[cfg(unix)]
742fn restore_sigpipe_default() {
743 unsafe {
746 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
747 }
748}
749
750#[cfg(not(unix))]
751fn restore_sigpipe_default() {}
752
753fn passthrough(command: &str) -> ! {
754 let (shell, flag) = shell::shell_and_flag();
755 let mut cmd = std::process::Command::new(&shell);
756 cmd.arg(&flag).arg(command).env("LEAN_CTX_ACTIVE", "1");
757 shell::platform::apply_utf8_locale(&mut cmd);
758 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
759 std::process::exit(status);
760}
761
762pub(super) fn run_async<F: std::future::Future>(future: F) -> F::Output {
763 tokio::runtime::Runtime::new()
764 .expect("failed to create async runtime")
765 .block_on(future)
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use serial_test::serial;
772
773 fn args_of(parts: &[&str]) -> Vec<String> {
774 parts.iter().map(|s| (*s).to_string()).collect()
775 }
776
777 #[test]
778 fn server_modes_keep_ignored_sigpipe() {
779 for mode in ["mcp", "daemon", "proxy", "serve", "watch", "dashboard"] {
780 assert!(
781 is_server_mode(&args_of(&["lean-ctx", mode])),
782 "{mode} must count as server mode"
783 );
784 }
785 assert!(is_server_mode(&args_of(&["lean-ctx"])));
787 }
788
789 #[test]
790 fn cli_modes_restore_default_sigpipe() {
791 for mode in ["doctor", "-c", "status", "ls", "grep", "gain", "help"] {
792 assert!(
793 !is_server_mode(&args_of(&["lean-ctx", mode])),
794 "{mode} must count as CLI mode (SIGPIPE default)"
795 );
796 }
797 }
798
799 #[test]
800 fn quickstart_is_short_and_points_to_setup() {
801 let q = quickstart_text();
802 assert!(
803 q.contains("lean-ctx onboard"),
804 "quickstart must point to onboard"
805 );
806 assert!(q.contains("lean-ctx help"), "quickstart must point to help");
807 assert!(
809 q.lines().count() <= 16,
810 "quickstart should be short; got {} lines",
811 q.lines().count()
812 );
813 assert!(
814 !q.contains("COMMANDS:"),
815 "quickstart must not inline the full command reference"
816 );
817 }
818
819 #[test]
820 fn concise_help_is_short_and_points_to_full() {
821 let h = concise_help_text();
822 assert!(h.contains("lean-ctx onboard"), "must lead with onboard");
823 assert!(
824 h.contains("lean-ctx help all"),
825 "must point to full reference"
826 );
827 assert!(
828 h.contains("lean-ctx tools"),
829 "must surface the tools profile command"
830 );
831 assert!(
833 h.lines().count() <= 40,
834 "concise help should stay short; got {} lines",
835 h.lines().count()
836 );
837 assert!(
838 !h.contains("SHELL HOOK PATTERNS"),
839 "concise help must not inline the full pattern catalog"
840 );
841 }
842
843 #[test]
844 fn capability_banner_tool_count_matches_registry() {
845 let n = crate::server::registry::tool_count();
846 let banner = capability_banner();
847 assert!(
848 banner.contains(&format!("{n} MCP tools")),
849 "banner must show the live registry count ({n}); got: {banner}"
850 );
851 }
852
853 #[test]
854 #[serial]
855 fn worker_threads_default_clamps_low() {
856 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
857 assert_eq!(resolve_worker_threads(1), 1);
858 }
859
860 #[test]
861 #[serial]
862 fn worker_threads_default_clamps_high() {
863 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
864 assert_eq!(resolve_worker_threads(32), 4);
865 }
866
867 #[test]
868 #[serial]
869 fn worker_threads_default_passthrough() {
870 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
871 assert_eq!(resolve_worker_threads(3), 3);
872 }
873
874 #[test]
875 #[serial]
876 fn worker_threads_env_override() {
877 std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
878 assert_eq!(resolve_worker_threads(2), 12);
879 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
880 }
881
882 #[test]
883 #[serial]
884 fn worker_threads_env_invalid_falls_back() {
885 std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
886 assert_eq!(resolve_worker_threads(3), 3);
887 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
888 }
889}