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