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;
10pub(crate) mod suggest;
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 if !is_server_mode(&args) {
34 restore_sigpipe_default();
35 }
36
37 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
38 if !enters_mcp {
39 crate::core::logging::init_logging();
40 }
41
42 if args.len() > 1 {
43 let rest = args[2..].to_vec();
44
45 match args[1].as_str() {
46 "-c" | "exec" => {
47 let raw = rest.first().is_some_and(|a| a == "--raw");
48 let cmd_args = if raw { &args[3..] } else { &args[2..] };
49 let command = if cmd_args.len() == 1 {
50 cmd_args[0].clone()
51 } else {
52 shell::join_command(cmd_args)
53 };
54 core::agent_runtime_env::capture();
58 if crate::shell::reentry::should_pass_through() {
59 passthrough(&command);
60 }
61 if raw {
62 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
65 } else {
66 unsafe { std::env::set_var("LEAN_CTX_COMPRESS", "1") };
69 }
70 let code = shell::exec(&command);
71 core::tool_lifecycle::flush_all();
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 crate::shell::reentry::should_pass_through() {
81 passthrough(&command);
82 }
83 shell::exec(&command)
84 };
85 core::tool_lifecycle::flush_all();
86 std::process::exit(code);
87 }
88 "shell" | "--shell" => {
89 shell::interactive();
90 return;
91 }
92 "gain" => {
93 cmd_gain(&rest);
94 return;
95 }
96 "spend" => {
97 cmd_spend(&rest);
98 return;
99 }
100 "savings" => {
101 cmd_savings(&rest);
102 return;
103 }
104 "learning" => {
105 cmd_learning(&rest);
106 return;
107 }
108 "conformance" | "selftest" => {
109 cmd_conformance(&rest);
110 return;
111 }
112 "health" => {
113 let code = crate::cli::health_cmd::cmd_health(&rest);
114 if code != 0 {
115 std::process::exit(code);
116 }
117 return;
118 }
119 "billing" => {
120 cmd_billing(&rest);
121 return;
122 }
123 "finops" => {
124 cmd_finops(&rest);
125 return;
126 }
127 "roi" => {
128 super::cmd_roi(&rest);
131 return;
132 }
133 "output-savings" | "output_savings" => {
134 super::cmd_output_savings(&rest);
137 return;
138 }
139 "token-report" | "report-tokens" => {
140 let code = token_report::run_cli(&rest);
141 if code != 0 {
142 std::process::exit(code);
143 }
144 return;
145 }
146 "pack" => {
147 crate::cli::cmd_pack(&rest);
148 return;
149 }
150 "policy" => {
151 crate::cli::cmd_policy(&rest);
152 return;
153 }
154 "plugin" | "plugins" => {
155 crate::cli::plugin_cmd::cmd_plugin(&rest);
156 return;
157 }
158 "addon" | "addons" => {
159 crate::cli::addon_cmd::cmd_addon(&rest);
160 return;
161 }
162 "embeddings" => {
163 crate::cli::embeddings_cmd::cmd_embeddings(&rest);
164 return;
165 }
166 "enable-gpu" | "gpu" => {
167 core::updater::enable_gpu(&rest);
168 return;
169 }
170 "rules" => {
171 crate::cli::rules_cmd::cmd_rules(&rest);
172 return;
173 }
174 "proof" => {
175 crate::cli::cmd_proof(&rest);
176 return;
177 }
178 "snapshot" => {
179 crate::cli::cmd_snapshot(&rest);
180 return;
181 }
182 "verify" => {
183 crate::cli::cmd_verify(&rest);
184 return;
185 }
186 "eval" => {
187 crate::cli::eval_cmd::cmd_eval(&rest);
188 return;
189 }
190 "verify-cache" | "cache-selftest" => {
191 let code = crate::cli::verify_cache_cmd::cmd_verify_cache(&rest);
192 if code != 0 {
193 std::process::exit(code);
194 }
195 return;
196 }
197 "visualize" => {
198 super::cmd_visualize(&rest);
199 return;
200 }
201 "audit" => {
202 if rest.first().map(String::as_str) == Some("evidence") {
203 crate::cli::audit_report::cmd_evidence(&rest[1..]);
204 } else {
205 println!("{}", crate::cli::audit_report::generate_report());
206 }
207 return;
208 }
209 "compliance" => {
210 crate::cli::cmd_compliance(&rest);
211 return;
212 }
213 "agent" => {
214 crate::cli::cmd_agent(&rest);
215 return;
216 }
217 "instructions" => {
218 crate::cli::cmd_instructions(&rest);
219 return;
220 }
221 "index" => {
222 crate::cli::cmd_index(&rest);
223 return;
224 }
225 "semantic-search" | "search-code" => {
226 crate::cli::cmd_semantic_search(&rest);
227 core::stats::flush();
228 return;
229 }
230 "explore" => {
231 crate::cli::explore_cmd::cmd_explore(&rest);
232 core::stats::flush();
233 return;
234 }
235 "repomap" | "repo-map" => {
236 crate::cli::cmd_repomap(&rest);
237 core::stats::flush();
238 return;
239 }
240 "cep" => {
241 println!("{}", core::stats::format_cep_report());
242 return;
243 }
244 "dashboard" => {
245 cmd_dashboard(&rest);
246 return;
247 }
248 "team" => {
249 cmd_team(&rest);
250 return;
251 }
252 "provider" => {
253 cmd_provider(&rest);
254 return;
255 }
256 "serve" => {
257 cmd_serve(&rest);
258 return;
259 }
260 "watch" => {
261 cmd_watch(&rest);
262 return;
263 }
264 "proxy" => {
265 cmd_proxy(&rest);
266 return;
267 }
268 #[cfg(feature = "gateway-server")]
269 "gateway" => {
270 cmd_gateway(&rest);
271 return;
272 }
273 "daemon" => {
274 cmd_daemon(&rest);
275 return;
276 }
277 "init" => {
278 super::cmd_init(&rest);
279 return;
280 }
281 "setup" => {
282 if rest.iter().any(|a| a == "--help" || a == "-h") {
286 print_setup_help();
287 return;
288 }
289 const KNOWN: &[&str] = &[
290 "--non-interactive",
291 "--yes",
292 "-y",
293 "--fix",
294 "--json",
295 "--no-auto-approve",
296 "--skip-rules",
297 "--no-agent-aliases",
298 ];
299 if let Some(unknown) = rest
300 .iter()
301 .find(|a| a.starts_with('-') && !KNOWN.contains(&a.as_str()))
302 {
303 eprintln!("setup: unknown flag '{unknown}'\n");
304 print_setup_help();
305 std::process::exit(2);
306 }
307 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
308 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
309 let fix = rest.iter().any(|a| a == "--fix");
310 let json = rest.iter().any(|a| a == "--json");
311 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
312 let skip_rules = rest.iter().any(|a| a == "--skip-rules");
313 let no_agent_aliases = rest.iter().any(|a| a == "--no-agent-aliases");
314
315 if no_agent_aliases {
316 let _ = crate::core::config::setter::set_by_key("skip_agent_aliases", "true");
317 }
318
319 if non_interactive || fix || json || yes {
320 let opts = setup::SetupOptions {
321 non_interactive,
322 yes,
323 fix,
324 json,
325 no_auto_approve,
326 skip_rules,
327 ..Default::default()
328 };
329 match setup::run_setup_with_options(opts) {
330 Ok(report) => {
331 if json {
332 println!(
333 "{}",
334 serde_json::to_string_pretty(&report)
335 .unwrap_or_else(|_| "{}".to_string())
336 );
337 }
338 if !report.success {
339 std::process::exit(1);
340 }
341 }
342 Err(e) => {
343 eprintln!("{e}");
344 std::process::exit(1);
345 }
346 }
347 } else {
348 setup::run_setup();
349 }
350 return;
351 }
352 "onboard" => {
353 if rest.iter().any(|a| a == "--help" || a == "-h") {
354 println!("Usage: lean-ctx onboard [--no-agent-aliases]");
355 println!("Connect your AI tools with one command: detects installed");
356 println!("agents, installs hooks/rules/MCP registrations, verifies.");
357 println!();
358 println!(
359 " --no-agent-aliases Do not install claude/codex/gemini shell aliases"
360 );
361 println!();
362 println!("Fine-grained control: lean-ctx setup --help");
363 return;
364 }
365 if rest.iter().any(|a| a == "--no-agent-aliases") {
366 let _ = crate::core::config::setter::set_by_key("skip_agent_aliases", "true");
367 }
368 setup::run_onboard();
369 return;
370 }
371 "install" => {
372 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
377 let json = rest.iter().any(|a| a == "--json");
378 if !repair {
379 setup::run_setup();
380 return;
381 }
382 let opts = setup::SetupOptions {
383 non_interactive: true,
384 yes: true,
385 fix: true,
386 json,
387 ..Default::default()
388 };
389 match setup::run_setup_with_options(opts) {
390 Ok(report) => {
391 if json {
392 println!(
393 "{}",
394 serde_json::to_string_pretty(&report)
395 .unwrap_or_else(|_| "{}".to_string())
396 );
397 }
398 if !report.success {
399 std::process::exit(1);
400 }
401 }
402 Err(e) => {
403 eprintln!("{e}");
404 std::process::exit(1);
405 }
406 }
407 return;
408 }
409 "bootstrap" => {
410 let json = rest.iter().any(|a| a == "--json");
411 let opts = setup::SetupOptions {
412 non_interactive: true,
413 yes: true,
414 fix: true,
415 json,
416 ..Default::default()
417 };
418 match setup::run_setup_with_options(opts) {
419 Ok(report) => {
420 if json {
421 println!(
422 "{}",
423 serde_json::to_string_pretty(&report)
424 .unwrap_or_else(|_| "{}".to_string())
425 );
426 }
427 if !report.success {
428 std::process::exit(1);
429 }
430 }
431 Err(e) => {
432 eprintln!("{e}");
433 std::process::exit(1);
434 }
435 }
436 return;
437 }
438 "wrap" => {
439 crate::wrap::run_wrap(&rest);
440 return;
441 }
442 "unwrap" => {
443 crate::wrap::run_unwrap(&rest);
444 return;
445 }
446 "status" => {
447 let code = status::run_cli(&rest);
448 if code != 0 {
449 std::process::exit(code);
450 }
451 return;
452 }
453 "read" => {
454 super::cmd_read(&rest);
455 core::tool_lifecycle::flush_all();
456 return;
457 }
458 "call" => {
459 super::cmd_call(&rest);
460 return;
461 }
462 "diff" => {
463 super::cmd_diff(&rest);
464 core::tool_lifecycle::flush_all();
465 return;
466 }
467 "grep" => {
468 super::cmd_grep(&rest);
469 core::tool_lifecycle::flush_all();
470 return;
471 }
472 "glob" => {
473 super::cmd_glob(&rest);
474 core::stats::flush();
475 return;
476 }
477 "find" => {
478 super::cmd_find(&rest);
479 core::tool_lifecycle::flush_all();
480 return;
481 }
482 "ls" => {
483 super::cmd_ls(&rest);
484 core::tool_lifecycle::flush_all();
485 return;
486 }
487 "deps" => {
488 super::cmd_deps(&rest);
489 core::tool_lifecycle::flush_all();
490 return;
491 }
492 "discover" => {
493 super::cmd_discover(&rest);
494 return;
495 }
496 "ghost" => {
497 super::cmd_ghost(&rest);
498 return;
499 }
500 "filter" => {
501 super::cmd_filter(&rest);
502 return;
503 }
504 "heatmap" => {
505 heatmap::cmd_heatmap(&rest);
506 return;
507 }
508 "graph" => {
509 cmd_graph(&rest);
510 return;
511 }
512 "smells" => {
513 cmd_smells(&rest);
514 return;
515 }
516 "session" => {
517 super::cmd_session_action(&rest);
518 return;
519 }
520 "ledger" => {
521 super::cmd_ledger(&rest);
522 return;
523 }
524 "control" | "context-control" => {
525 super::cmd_control(&rest);
526 return;
527 }
528 "plan" | "context-plan" => {
529 super::cmd_plan(&rest);
530 return;
531 }
532 "compile" | "context-compile" => {
533 super::cmd_compile(&rest);
534 return;
535 }
536 "knowledge" => {
537 super::cmd_knowledge(&rest);
538 return;
539 }
540 "skillify" => {
541 super::cmd_skillify(&rest);
542 return;
543 }
544 "summary" => {
545 super::cmd_summary(&rest);
546 return;
547 }
548 "overview" => {
549 super::cmd_overview(&rest);
550 return;
551 }
552 "compress" => {
553 super::cmd_compress(&rest);
554 return;
555 }
556 "wrapped" => {
557 eprintln!("'lean-ctx wrapped' has been removed. Use: lean-ctx gain --wrapped");
558 std::process::exit(1);
559 }
560 "sessions" | "session-store" => {
561 super::cmd_sessions(&rest);
562 return;
563 }
564 "benchmark" => {
565 super::cmd_benchmark(&rest);
566 return;
567 }
568 "compact" => {
569 cmd_compact(&rest);
570 return;
571 }
572 "profile" => {
573 super::cmd_profile(&rest);
574 return;
575 }
576 "tools" => {
577 if rest.first().map(String::as_str) == Some("health") {
580 super::cmd_tools_health(&rest[1..]);
581 return;
582 }
583 let mut forwarded = vec!["tools".to_string()];
587 forwarded.extend(rest.iter().cloned());
588 super::cmd_profile(&forwarded);
589 return;
590 }
591 "config" => {
592 super::cmd_config(&rest);
593 return;
594 }
595 "allow" => {
596 super::cmd_allow(&rest);
597 return;
598 }
599 "security" => {
600 super::cmd_security(&rest);
601 return;
602 }
603 "yolo" => {
604 super::cmd_yolo(&rest);
605 return;
606 }
607 "secure" | "lockdown" => {
608 super::cmd_secure(&rest);
609 return;
610 }
611 "trust" => {
612 super::cmd_trust(&rest);
613 return;
614 }
615 "untrust" => {
616 super::cmd_untrust(&rest);
617 return;
618 }
619 "stats" => {
620 super::cmd_stats(&rest);
621 return;
622 }
623 "introspect" => {
624 super::cmd_introspect(&rest);
625 return;
626 }
627 "cache" => {
628 super::cmd_cache(&rest);
629 return;
630 }
631 "theme" => {
632 super::cmd_theme(&rest);
633 return;
634 }
635 "tee" => {
636 super::cmd_tee(&rest);
637 return;
638 }
639 "terse" | "compression" => {
640 super::cmd_compression(&rest);
641 return;
642 }
643 "slow-log" => {
644 super::cmd_slow_log(&rest);
645 return;
646 }
647 "debug-log" => {
648 super::cmd_debug_log(&rest);
649 return;
650 }
651 "editor-signal" => {
654 let file = rest
655 .iter()
656 .position(|a| a == "--file")
657 .and_then(|i| rest.get(i + 1));
658 if let Some(path) = file {
659 if let Err(e) = core::editor_signal::record_focus(path) {
660 eprintln!("editor-signal: {e}");
661 std::process::exit(1);
662 }
663 } else {
664 eprintln!("usage: lean-ctx editor-signal --file <path>");
665 std::process::exit(2);
666 }
667 return;
668 }
669 "update" | "--self-update" => {
670 core::updater::run(&rest);
671 return;
672 }
673 "restart" => {
674 cmd_restart();
675 return;
676 }
677 "stop" => {
678 cmd_stop();
679 return;
680 }
681 "dev-install" => {
682 cmd_dev_install();
683 return;
684 }
685 "codesign-setup" => {
686 cmd_codesign_setup();
687 return;
688 }
689 "doctor" => {
690 let code = doctor::run_cli(&rest);
691 if code != 0 {
692 std::process::exit(code);
693 }
694 return;
695 }
696 "harden" => {
697 super::harden::run(&rest);
698 return;
699 }
700 "export-rules" => {
701 super::export_rules::run(&rest);
702 return;
703 }
704 "completions" => {
705 super::completions::run_completions(&rest);
706 return;
707 }
708 "__complete" => {
709 #[allow(non_snake_case)]
710 super::completions::run___complete(&rest);
711 return;
712 }
713 "gotchas" | "bugs" => {
714 super::cloud::cmd_gotchas(&rest);
715 return;
716 }
717 "learn" => {
718 super::cmd_learn(&rest);
719 return;
720 }
721 "buddy" | "pet" => {
722 super::cloud::cmd_buddy(&rest);
723 return;
724 }
725 "hook" => {
726 hook_handlers::mark_hook_environment();
727 core::agent_runtime_env::capture();
731 let action = rest.first().map_or("help", std::string::String::as_str);
732 if !matches!(action, "rewrite" | "redirect") {
737 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
738 }
739 match action {
740 "rewrite" => hook_handlers::handle_rewrite(),
741 "redirect" => hook_handlers::handle_redirect(),
742 "read-dedup" => hook_handlers::handle_read_dedup(),
743 "observe" => hook_handlers::handle_observe(),
744 "copilot" => hook_handlers::handle_copilot(),
745 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
746 "codex-session-start" => hook_handlers::handle_codex_session_start(),
747 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
748 _ => {
749 eprintln!(
750 "Usage: lean-ctx hook <rewrite|redirect|read-dedup|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>"
751 );
752 eprintln!(
753 " Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)"
754 );
755 std::process::exit(1);
756 }
757 }
758 return;
759 }
760 "report-issue" | "report" => {
761 report::run(&rest);
762 return;
763 }
764 "uninstall" => {
765 if rest.iter().any(|a| a == "--help" || a == "-h") {
768 uninstall::print_help();
769 return;
770 }
771 let dry_run = rest.iter().any(|a| a == "--dry-run");
772 let keep_config = rest.iter().any(|a| a == "--keep-config");
773 let keep_binary = rest.iter().any(|a| a == "--keep-binary");
774 uninstall::run(dry_run, keep_config, keep_binary);
775 return;
776 }
777 "raw" | "bypass" => {
782 if rest.is_empty() {
783 eprintln!("Usage: lean-ctx raw \"command\"");
784 eprintln!(
785 "Runs the command with output passed through unchanged (no \
786 compression). The shell allowlist still applies."
787 );
788 std::process::exit(1);
789 }
790 let command = if rest.len() == 1 {
791 rest[0].clone()
792 } else {
793 shell::join_command(&args[2..])
794 };
795 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
798 let code = shell::exec(&command);
799 std::process::exit(code);
800 }
801 "safety-levels" | "safety" => {
802 println!("{}", core::compression_safety::format_safety_table());
803 return;
804 }
805 "cheat" | "cheatsheet" | "cheat-sheet" => {
806 super::cmd_cheatsheet();
807 return;
808 }
809 "login" => {
810 super::cloud::cmd_login(&rest);
811 return;
812 }
813 "register" => {
814 super::cloud::cmd_register(&rest);
815 return;
816 }
817 "forgot-password" => {
818 super::cloud::cmd_forgot_password(&rest);
819 return;
820 }
821 "sync" => {
822 super::cloud::cmd_sync(&rest);
823 return;
824 }
825 "contribute" => {
826 super::cloud::cmd_contribute();
827 return;
828 }
829 "cloud" => {
830 super::cloud::cmd_cloud(&rest);
831 return;
832 }
833 "upgrade" => {
834 super::cloud::cmd_upgrade();
835 return;
836 }
837 "--version" | "-V" => {
838 println!("{}", core::integrity::origin_line());
839 return;
840 }
841 "help" => {
842 let want_all = rest
843 .iter()
844 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"));
845 if want_all {
846 print_help();
847 } else {
848 print_help_concise();
849 }
850 return;
851 }
852 "--help" | "-h" => {
853 if rest
854 .iter()
855 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"))
856 {
857 print_help();
858 } else {
859 print_help_concise();
860 }
861 return;
862 }
863 "mcp" => {}
864 _ => {
865 let unknown = &args[1];
866 eprintln!("lean-ctx: unknown command '{unknown}'");
867 if let Some(suggestion) = suggest::closest_command(unknown) {
868 eprintln!(" did you mean '{suggestion}'?");
869 }
870 eprintln!(" run 'lean-ctx help' for the full command list");
871 std::process::exit(1);
872 }
873 }
874 }
875
876 if args.len() == 1 && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
882 print_quickstart();
883 return;
884 }
885
886 if let Err(e) = run_mcp_server() {
887 tracing::error!("lean-ctx: {e}");
888 std::process::exit(1);
889 }
890}
891
892fn print_setup_help() {
898 println!("Usage: lean-ctx setup [options]");
899 println!();
900 println!("Guided setup: shell hook, agent hooks/rules, MCP registrations.");
901 println!("Interactive by default; runs non-interactively without a TTY.");
902 println!();
903 println!("Options:");
904 println!(" --non-interactive No prompts; apply defaults");
905 println!(" --yes, -y Assume yes for all prompts");
906 println!(" --fix Repair an existing installation");
907 println!(" --json Machine-readable report (implies non-interactive)");
908 println!(" --no-auto-approve Skip auto-approve configuration");
909 println!(" --skip-rules Do not write agent rules files");
910 println!(" --help, -h Show this help (never runs setup)");
911 println!();
912 println!("See also: lean-ctx onboard (one-command setup), lean-ctx doctor");
913}
914
915fn is_server_mode(args: &[String]) -> bool {
916 args.len() == 1
917 || args.get(1).is_some_and(|a| {
918 matches!(
919 a.as_str(),
920 "mcp" | "daemon" | "proxy" | "serve" | "watch" | "dashboard" | "gateway"
921 )
922 })
923}
924
925#[cfg(unix)]
935fn restore_sigpipe_default() {
936 unsafe {
939 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
940 }
941}
942
943#[cfg(not(unix))]
944fn restore_sigpipe_default() {}
945
946fn passthrough(command: &str) -> ! {
947 let (shell, flag) = shell::shell_and_flag();
948 let mut cmd = std::process::Command::new(&shell);
949 cmd.arg(&flag).arg(command);
950 shell::reentry::mark_child(&mut cmd);
951 shell::platform::apply_utf8_locale(&mut cmd);
952 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
953 std::process::exit(status);
954}
955
956pub(super) fn run_async<F: std::future::Future>(future: F) -> F::Output {
957 match tokio::runtime::Runtime::new() {
960 Ok(rt) => rt.block_on(future),
961 Err(e) => {
962 eprintln!("lean-ctx: failed to create async runtime: {e}");
963 std::process::exit(1);
964 }
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971 use serial_test::serial;
972
973 fn args_of(parts: &[&str]) -> Vec<String> {
974 parts.iter().map(|s| (*s).to_string()).collect()
975 }
976
977 #[test]
978 fn server_modes_keep_ignored_sigpipe() {
979 for mode in ["mcp", "daemon", "proxy", "serve", "watch", "dashboard"] {
980 assert!(
981 is_server_mode(&args_of(&["lean-ctx", mode])),
982 "{mode} must count as server mode"
983 );
984 }
985 assert!(is_server_mode(&args_of(&["lean-ctx"])));
987 }
988
989 #[test]
990 fn cli_modes_restore_default_sigpipe() {
991 for mode in ["doctor", "-c", "status", "ls", "grep", "gain", "help"] {
992 assert!(
993 !is_server_mode(&args_of(&["lean-ctx", mode])),
994 "{mode} must count as CLI mode (SIGPIPE default)"
995 );
996 }
997 }
998
999 #[test]
1000 fn quickstart_is_short_and_points_to_setup() {
1001 let q = quickstart_text();
1002 assert!(q.contains("lean-ctx wrap"), "quickstart must point to wrap");
1003 assert!(q.contains("lean-ctx help"), "quickstart must point to help");
1004 assert!(
1006 q.lines().count() <= 16,
1007 "quickstart should be short; got {} lines",
1008 q.lines().count()
1009 );
1010 assert!(
1011 !q.contains("COMMANDS:"),
1012 "quickstart must not inline the full command reference"
1013 );
1014 }
1015
1016 #[test]
1017 fn concise_help_is_short_and_points_to_full() {
1018 let h = concise_help_text();
1019 assert!(h.contains("lean-ctx wrap"), "must lead with wrap");
1020 assert!(
1021 h.contains("lean-ctx help all"),
1022 "must point to full reference"
1023 );
1024 assert!(
1025 h.contains("lean-ctx tools"),
1026 "must surface the tools profile command"
1027 );
1028 assert!(
1030 h.lines().count() <= 40,
1031 "concise help should stay short; got {} lines",
1032 h.lines().count()
1033 );
1034 assert!(
1035 !h.contains("SHELL HOOK PATTERNS"),
1036 "concise help must not inline the full pattern catalog"
1037 );
1038 }
1039
1040 #[test]
1041 fn capability_banner_tool_count_matches_registry() {
1042 let n = crate::server::registry::tool_count();
1043 let banner = capability_banner();
1044 assert!(
1045 banner.contains(&format!("{n} MCP tools")),
1046 "banner must show the live registry count ({n}); got: {banner}"
1047 );
1048 }
1049
1050 #[test]
1051 #[serial]
1052 fn worker_threads_default_clamps_low() {
1053 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
1054 assert_eq!(resolve_worker_threads(1), 1);
1055 }
1056
1057 #[test]
1058 #[serial]
1059 fn worker_threads_default_clamps_high() {
1060 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
1061 assert_eq!(resolve_worker_threads(32), 4);
1062 }
1063
1064 #[test]
1065 #[serial]
1066 fn worker_threads_default_passthrough() {
1067 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
1068 assert_eq!(resolve_worker_threads(3), 3);
1069 }
1070
1071 #[test]
1072 #[serial]
1073 fn worker_threads_env_override() {
1074 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "12");
1075 assert_eq!(resolve_worker_threads(2), 12);
1076 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
1077 }
1078
1079 #[test]
1080 #[serial]
1081 fn worker_threads_env_invalid_falls_back() {
1082 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
1083 assert_eq!(resolve_worker_threads(3), 3);
1084 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
1085 }
1086}