1use std::io::{self, IsTerminal, Write};
2use std::process::{Command, Stdio};
3
4use crate::core::config;
5use crate::core::slow_log;
6use crate::core::tokens::count_tokens;
7
8pub fn exec_argv(args: &[String]) -> i32 {
14 if args.is_empty() {
15 return 127;
16 }
17
18 if std::env::var("LEAN_CTX_DISABLED").is_ok() || std::env::var("LEAN_CTX_ACTIVE").is_ok() {
19 return exec_direct(args);
20 }
21
22 let joined = super::platform::join_command(args);
23 let cfg = config::Config::load();
24
25 if super::compress::is_excluded_command(&joined, &cfg.excluded_commands) {
26 let code = exec_direct(args);
27 crate::core::tool_lifecycle::record_shell_command(0, 0);
28 return code;
29 }
30
31 let code = exec_direct(args);
32 crate::core::tool_lifecycle::record_shell_command(0, 0);
33 code
34}
35
36fn exec_direct(args: &[String]) -> i32 {
37 let status = Command::new(&args[0])
38 .args(&args[1..])
39 .env("LEAN_CTX_ACTIVE", "1")
40 .stdin(Stdio::inherit())
41 .stdout(Stdio::inherit())
42 .stderr(Stdio::inherit())
43 .status();
44
45 match status {
46 Ok(s) => s.code().unwrap_or(1),
47 Err(e) => {
48 tracing::error!("lean-ctx: failed to execute: {e}");
49 127
50 }
51 }
52}
53
54pub fn exec(command: &str) -> i32 {
55 let (shell, shell_flag) = super::platform::shell_and_flag();
56 let command = crate::tools::ctx_shell::normalize_command_for_shell(command);
57 let command = command.as_str();
58
59 if std::env::var("LEAN_CTX_DISABLED").is_ok() || std::env::var("LEAN_CTX_ACTIVE").is_ok() {
60 return exec_inherit(command, &shell, &shell_flag);
61 }
62
63 let cfg = config::Config::load();
64 let force_compress = std::env::var("LEAN_CTX_COMPRESS").is_ok();
65 let raw_mode = std::env::var("LEAN_CTX_RAW").is_ok();
66
67 if raw_mode
68 || (!force_compress
69 && super::compress::is_excluded_command(command, &cfg.excluded_commands))
70 {
71 return exec_inherit_tracked(command, &shell, &shell_flag);
72 }
73
74 if !force_compress {
75 if io::stdout().is_terminal() {
76 return exec_inherit_tracked(command, &shell, &shell_flag);
77 }
78 let code = exec_inherit(command, &shell, &shell_flag);
79 crate::core::tool_lifecycle::record_shell_command(0, 0);
80 return code;
81 }
82
83 exec_buffered(command, &shell, &shell_flag, &cfg)
84}
85
86fn exec_inherit(command: &str, shell: &str, shell_flag: &str) -> i32 {
87 let status = Command::new(shell)
88 .arg(shell_flag)
89 .arg(command)
90 .env("LEAN_CTX_ACTIVE", "1")
91 .stdin(Stdio::inherit())
92 .stdout(Stdio::inherit())
93 .stderr(Stdio::inherit())
94 .status();
95
96 match status {
97 Ok(s) => s.code().unwrap_or(1),
98 Err(e) => {
99 tracing::error!("lean-ctx: failed to execute: {e}");
100 127
101 }
102 }
103}
104
105fn exec_inherit_tracked(command: &str, shell: &str, shell_flag: &str) -> i32 {
106 let code = exec_inherit(command, shell, shell_flag);
107 crate::core::tool_lifecycle::record_shell_command(0, 0);
108 code
109}
110
111fn combine_output(stdout: &str, stderr: &str) -> String {
112 if stderr.is_empty() {
113 stdout.to_string()
114 } else if stdout.is_empty() {
115 stderr.to_string()
116 } else {
117 format!("{stdout}\n{stderr}")
118 }
119}
120
121fn exec_buffered(command: &str, shell: &str, shell_flag: &str, cfg: &config::Config) -> i32 {
122 #[cfg(windows)]
123 super::platform::set_console_utf8();
124
125 let start = std::time::Instant::now();
126
127 let mut cmd = Command::new(shell);
128 cmd.arg(shell_flag);
129
130 #[cfg(windows)]
131 let ps_tmp_path: Option<tempfile::TempPath>;
132 #[cfg(windows)]
133 {
134 let is_powershell =
135 shell.to_lowercase().contains("powershell") || shell.to_lowercase().contains("pwsh");
136 if is_powershell {
137 let ps_script = format!(
138 "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {}",
139 command
140 );
141 let tmp = tempfile::Builder::new()
142 .prefix("lean-ctx-ps-")
143 .suffix(".ps1")
144 .tempfile()
145 .expect("failed to create temp file for PowerShell script");
146 let tmp_path = tmp.into_temp_path();
147 let _ = std::fs::write(&tmp_path, &ps_script);
148 cmd.args([
149 "-ExecutionPolicy",
150 "Bypass",
151 "-File",
152 &tmp_path.to_string_lossy(),
153 ]);
154 ps_tmp_path = Some(tmp_path);
155 } else {
156 cmd.arg(command);
157 ps_tmp_path = None;
158 }
159 }
160 #[cfg(not(windows))]
161 cmd.arg(command);
162
163 let child = cmd
164 .env("LEAN_CTX_ACTIVE", "1")
165 .stdout(Stdio::piped())
166 .stderr(Stdio::piped())
167 .spawn();
168
169 let child = match child {
170 Ok(c) => c,
171 Err(e) => {
172 tracing::error!("lean-ctx: failed to execute: {e}");
173 #[cfg(windows)]
174 if let Some(ref tmp) = ps_tmp_path {
175 let _ = std::fs::remove_file(tmp);
176 }
177 return 127;
178 }
179 };
180
181 let output = match child.wait_with_output() {
182 Ok(o) => o,
183 Err(e) => {
184 tracing::error!("lean-ctx: failed to wait: {e}");
185 #[cfg(windows)]
186 if let Some(ref tmp) = ps_tmp_path {
187 let _ = std::fs::remove_file(tmp);
188 }
189 return 127;
190 }
191 };
192
193 let duration_ms = start.elapsed().as_millis();
194 let exit_code = output.status.code().unwrap_or(1);
195 let stdout = super::platform::decode_output(&output.stdout);
196 let stderr = super::platform::decode_output(&output.stderr);
197
198 let full_output = combine_output(&stdout, &stderr);
199 let input_tokens = count_tokens(&full_output);
200
201 let (compressed, output_tokens) =
202 super::compress::compress_and_measure(command, &stdout, &stderr);
203
204 crate::core::tool_lifecycle::record_shell_command(input_tokens, output_tokens);
205
206 if !compressed.is_empty() {
207 let _ = io::stdout().write_all(compressed.as_bytes());
208 if !compressed.ends_with('\n') {
209 let _ = io::stdout().write_all(b"\n");
210 }
211 }
212 let should_tee = match cfg.tee_mode {
213 config::TeeMode::Always => !full_output.trim().is_empty(),
214 config::TeeMode::Failures => exit_code != 0 && !full_output.trim().is_empty(),
215 config::TeeMode::Never => false,
216 };
217 if should_tee {
218 if let Some(path) = super::redact::save_tee(command, &full_output) {
219 eprintln!("[lean-ctx: full output -> {path} (redacted, 24h TTL)]");
220 }
221 }
222
223 let threshold = cfg.slow_command_threshold_ms;
224 if threshold > 0 && duration_ms >= threshold as u128 {
225 slow_log::record(command, duration_ms, exit_code);
226 }
227
228 #[cfg(windows)]
229 if let Some(ref tmp) = ps_tmp_path {
230 let _ = std::fs::remove_file(tmp);
231 }
232
233 exit_code
234}
235
236#[cfg(test)]
237mod exec_tests {
238 #[test]
239 fn exec_direct_runs_true() {
240 let code = super::exec_direct(&["true".to_string()]);
241 assert_eq!(code, 0);
242 }
243
244 #[test]
245 fn exec_direct_runs_false() {
246 let code = super::exec_direct(&["false".to_string()]);
247 assert_ne!(code, 0);
248 }
249
250 #[test]
251 fn exec_direct_preserves_args_with_special_chars() {
252 let code = super::exec_direct(&[
253 "echo".to_string(),
254 "hello world".to_string(),
255 "it's here".to_string(),
256 "a \"quoted\" thing".to_string(),
257 ]);
258 assert_eq!(code, 0);
259 }
260
261 #[test]
262 fn exec_direct_nonexistent_returns_127() {
263 let code = super::exec_direct(&["__nonexistent_binary_12345__".to_string()]);
264 assert_eq!(code, 127);
265 }
266
267 #[test]
268 fn exec_argv_empty_returns_127() {
269 let code = super::exec_argv(&[]);
270 assert_eq!(code, 127);
271 }
272
273 #[test]
274 fn exec_argv_runs_simple_command() {
275 let code = super::exec_argv(&["true".to_string()]);
276 assert_eq!(code, 0);
277 }
278
279 #[test]
280 fn exec_argv_passes_through_when_disabled() {
281 std::env::set_var("LEAN_CTX_DISABLED", "1");
282 let code = super::exec_argv(&["true".to_string()]);
283 std::env::remove_var("LEAN_CTX_DISABLED");
284 assert_eq!(code, 0);
285 }
286}