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