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