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