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