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