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