1use std::io::{self, BufRead, IsTerminal, Write};
2use std::process::{Command, Stdio};
3
4use crate::core::config;
5use crate::core::patterns;
6use crate::core::slow_log;
7use crate::core::stats;
8use crate::core::tokens::count_tokens;
9
10pub fn decode_output(bytes: &[u8]) -> String {
11 match String::from_utf8(bytes.to_vec()) {
12 Ok(s) => s,
13 Err(_) => {
14 #[cfg(windows)]
15 {
16 decode_windows_output(bytes)
17 }
18 #[cfg(not(windows))]
19 {
20 String::from_utf8_lossy(bytes).into_owned()
21 }
22 }
23 }
24}
25
26#[cfg(windows)]
27fn decode_windows_output(bytes: &[u8]) -> String {
28 use std::os::windows::ffi::OsStringExt;
29
30 extern "system" {
31 fn GetACP() -> u32;
32 fn MultiByteToWideChar(
33 cp: u32,
34 flags: u32,
35 src: *const u8,
36 srclen: i32,
37 dst: *mut u16,
38 dstlen: i32,
39 ) -> i32;
40 }
41
42 let codepage = unsafe { GetACP() };
43 let wide_len = unsafe {
44 MultiByteToWideChar(
45 codepage,
46 0,
47 bytes.as_ptr(),
48 bytes.len() as i32,
49 std::ptr::null_mut(),
50 0,
51 )
52 };
53 if wide_len <= 0 {
54 return String::from_utf8_lossy(bytes).into_owned();
55 }
56 let mut wide: Vec<u16> = vec![0u16; wide_len as usize];
57 unsafe {
58 MultiByteToWideChar(
59 codepage,
60 0,
61 bytes.as_ptr(),
62 bytes.len() as i32,
63 wide.as_mut_ptr(),
64 wide_len,
65 );
66 }
67 std::ffi::OsString::from_wide(&wide)
68 .to_string_lossy()
69 .into_owned()
70}
71
72#[cfg(windows)]
73fn set_console_utf8() {
74 extern "system" {
75 fn SetConsoleOutputCP(id: u32) -> i32;
76 }
77 unsafe {
78 SetConsoleOutputCP(65001);
79 }
80}
81
82pub fn is_container() -> bool {
84 #[cfg(unix)]
85 {
86 if std::path::Path::new("/.dockerenv").exists() {
87 return true;
88 }
89 if let Ok(cgroup) = std::fs::read_to_string("/proc/1/cgroup") {
90 if cgroup.contains("/docker/") || cgroup.contains("/lxc/") {
91 return true;
92 }
93 }
94 if let Ok(mounts) = std::fs::read_to_string("/proc/self/mountinfo") {
95 if mounts.contains("/docker/containers/") {
96 return true;
97 }
98 }
99 false
100 }
101 #[cfg(not(unix))]
102 {
103 false
104 }
105}
106
107pub fn is_non_interactive() -> bool {
109 !io::stdin().is_terminal()
110}
111
112pub fn exec(command: &str) -> i32 {
113 let (shell, shell_flag) = shell_and_flag();
114 let command = crate::tools::ctx_shell::normalize_command_for_shell(command);
115 let command = command.as_str();
116
117 if std::env::var("LEAN_CTX_DISABLED").is_ok() || std::env::var("LEAN_CTX_ACTIVE").is_ok() {
118 return exec_inherit(command, &shell, &shell_flag);
119 }
120
121 let cfg = config::Config::load();
122 let force_compress = std::env::var("LEAN_CTX_COMPRESS").is_ok();
123 let raw_mode = std::env::var("LEAN_CTX_RAW").is_ok();
124
125 if raw_mode || (!force_compress && is_excluded_command(command, &cfg.excluded_commands)) {
126 return exec_inherit(command, &shell, &shell_flag);
127 }
128
129 if !force_compress {
130 if io::stdout().is_terminal() {
131 return exec_inherit_tracked(command, &shell, &shell_flag);
132 }
133 return exec_inherit(command, &shell, &shell_flag);
134 }
135
136 exec_buffered(command, &shell, &shell_flag, &cfg)
137}
138
139fn exec_inherit(command: &str, shell: &str, shell_flag: &str) -> i32 {
140 let status = Command::new(shell)
141 .arg(shell_flag)
142 .arg(command)
143 .env("LEAN_CTX_ACTIVE", "1")
144 .stdin(Stdio::inherit())
145 .stdout(Stdio::inherit())
146 .stderr(Stdio::inherit())
147 .status();
148
149 match status {
150 Ok(s) => s.code().unwrap_or(1),
151 Err(e) => {
152 eprintln!("lean-ctx: failed to execute: {e}");
153 127
154 }
155 }
156}
157
158fn exec_inherit_tracked(command: &str, shell: &str, shell_flag: &str) -> i32 {
159 let code = exec_inherit(command, shell, shell_flag);
160 stats::record(command, 0, 0);
161 code
162}
163
164fn combine_output(stdout: &str, stderr: &str) -> String {
165 if stderr.is_empty() {
166 stdout.to_string()
167 } else if stdout.is_empty() {
168 stderr.to_string()
169 } else {
170 format!("{stdout}\n{stderr}")
171 }
172}
173
174fn exec_buffered(command: &str, shell: &str, shell_flag: &str, cfg: &config::Config) -> i32 {
175 #[cfg(windows)]
176 set_console_utf8();
177
178 let start = std::time::Instant::now();
179
180 let mut cmd = Command::new(shell);
181 cmd.arg(shell_flag);
182
183 #[cfg(windows)]
184 {
185 let is_powershell =
186 shell.to_lowercase().contains("powershell") || shell.to_lowercase().contains("pwsh");
187 if is_powershell {
188 cmd.arg(format!(
189 "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {command}"
190 ));
191 } else {
192 cmd.arg(command);
193 }
194 }
195 #[cfg(not(windows))]
196 cmd.arg(command);
197
198 let child = cmd
199 .env("LEAN_CTX_ACTIVE", "1")
200 .env_remove("DISPLAY")
201 .env_remove("XAUTHORITY")
202 .env_remove("WAYLAND_DISPLAY")
203 .stdout(Stdio::piped())
204 .stderr(Stdio::piped())
205 .spawn();
206
207 let child = match child {
208 Ok(c) => c,
209 Err(e) => {
210 eprintln!("lean-ctx: failed to execute: {e}");
211 return 127;
212 }
213 };
214
215 let output = match child.wait_with_output() {
216 Ok(o) => o,
217 Err(e) => {
218 eprintln!("lean-ctx: failed to wait: {e}");
219 return 127;
220 }
221 };
222
223 let duration_ms = start.elapsed().as_millis();
224 let exit_code = output.status.code().unwrap_or(1);
225 let stdout = decode_output(&output.stdout);
226 let stderr = decode_output(&output.stderr);
227
228 let full_output = combine_output(&stdout, &stderr);
229 let input_tokens = count_tokens(&full_output);
230
231 let (compressed, output_tokens) = compress_and_measure(command, &stdout, &stderr);
232
233 stats::record(command, input_tokens, output_tokens);
234
235 if !compressed.is_empty() {
236 let _ = io::stdout().write_all(compressed.as_bytes());
237 if !compressed.ends_with('\n') {
238 let _ = io::stdout().write_all(b"\n");
239 }
240 }
241 let should_tee = match cfg.tee_mode {
242 config::TeeMode::Always => !full_output.trim().is_empty(),
243 config::TeeMode::Failures => exit_code != 0 && !full_output.trim().is_empty(),
244 config::TeeMode::Never => false,
245 };
246 if should_tee {
247 if let Some(path) = save_tee(command, &full_output) {
248 eprintln!("[lean-ctx: full output -> {path} (redacted, 24h TTL)]");
249 }
250 }
251
252 let threshold = cfg.slow_command_threshold_ms;
253 if threshold > 0 && duration_ms >= threshold as u128 {
254 slow_log::record(command, duration_ms, exit_code);
255 }
256
257 exit_code
258}
259
260const BUILTIN_PASSTHROUGH: &[&str] = &[
261 "turbo",
263 "nx serve",
264 "nx dev",
265 "next dev",
266 "vite dev",
267 "vite preview",
268 "vitest",
269 "nuxt dev",
270 "astro dev",
271 "webpack serve",
272 "webpack-dev-server",
273 "nodemon",
274 "concurrently",
275 "pm2",
276 "pm2 logs",
277 "gatsby develop",
278 "expo start",
279 "react-scripts start",
280 "ng serve",
281 "remix dev",
282 "wrangler dev",
283 "hugo server",
284 "hugo serve",
285 "jekyll serve",
286 "bun dev",
287 "ember serve",
288 "npm run dev",
290 "npm run start",
291 "npm run serve",
292 "npm run watch",
293 "npm run preview",
294 "npm run storybook",
295 "npm run test:watch",
296 "npm start",
297 "npx ",
298 "pnpm run dev",
299 "pnpm run start",
300 "pnpm run serve",
301 "pnpm run watch",
302 "pnpm run preview",
303 "pnpm run storybook",
304 "pnpm dev",
305 "pnpm start",
306 "pnpm preview",
307 "yarn dev",
308 "yarn start",
309 "yarn serve",
310 "yarn watch",
311 "yarn preview",
312 "yarn storybook",
313 "bun run dev",
314 "bun run start",
315 "bun run serve",
316 "bun run watch",
317 "bun run preview",
318 "bun start",
319 "deno task dev",
320 "deno task start",
321 "deno task serve",
322 "deno run --watch",
323 "docker compose up",
325 "docker-compose up",
326 "docker compose logs",
327 "docker-compose logs",
328 "docker compose exec",
329 "docker-compose exec",
330 "docker compose run",
331 "docker-compose run",
332 "docker compose watch",
333 "docker-compose watch",
334 "docker logs",
335 "docker attach",
336 "docker exec -it",
337 "docker exec -ti",
338 "docker run -it",
339 "docker run -ti",
340 "docker stats",
341 "docker events",
342 "kubectl logs",
344 "kubectl exec -it",
345 "kubectl exec -ti",
346 "kubectl attach",
347 "kubectl port-forward",
348 "kubectl proxy",
349 "top",
351 "htop",
352 "btop",
353 "watch ",
354 "tail -f",
355 "tail -f ",
356 "journalctl -f",
357 "journalctl --follow",
358 "dmesg -w",
359 "dmesg --follow",
360 "strace",
361 "tcpdump",
362 "ping ",
363 "ping6 ",
364 "traceroute",
365 "mtr ",
366 "nmap ",
367 "iperf ",
368 "iperf3 ",
369 "ss -l",
370 "netstat -l",
371 "lsof -i",
372 "socat ",
373 "less",
375 "more",
376 "vim",
377 "nvim",
378 "vi ",
379 "nano",
380 "micro ",
381 "helix ",
382 "hx ",
383 "emacs",
384 "tmux",
386 "screen",
387 "ssh ",
389 "telnet ",
390 "nc ",
391 "ncat ",
392 "psql",
393 "mysql",
394 "sqlite3",
395 "redis-cli",
396 "mongosh",
397 "mongo ",
398 "python3 -i",
399 "python -i",
400 "irb",
401 "rails console",
402 "rails c ",
403 "iex",
404 "flask run",
406 "uvicorn ",
407 "gunicorn ",
408 "hypercorn ",
409 "daphne ",
410 "django-admin runserver",
411 "manage.py runserver",
412 "python manage.py runserver",
413 "python -m http.server",
414 "python3 -m http.server",
415 "streamlit run",
416 "gradio ",
417 "celery worker",
418 "celery -a",
419 "celery -b",
420 "dramatiq ",
421 "rq worker",
422 "watchmedo ",
423 "ptw ",
424 "pytest-watch",
425 "rails server",
427 "rails s",
428 "puma ",
429 "unicorn ",
430 "thin start",
431 "foreman start",
432 "overmind start",
433 "guard ",
434 "sidekiq",
435 "resque ",
436 "php artisan serve",
438 "php -s ",
439 "php artisan queue:work",
440 "php artisan queue:listen",
441 "php artisan horizon",
442 "php artisan tinker",
443 "sail up",
444 "./gradlew bootrun",
446 "gradlew bootrun",
447 "gradle bootrun",
448 "./gradlew run",
449 "mvn spring-boot:run",
450 "./mvnw spring-boot:run",
451 "mvnw spring-boot:run",
452 "mvn quarkus:dev",
453 "./mvnw quarkus:dev",
454 "sbt run",
455 "sbt ~compile",
456 "lein run",
457 "lein repl",
458 "go run ",
460 "air ",
461 "gin ",
462 "realize start",
463 "reflex ",
464 "gowatch ",
465 "dotnet run",
467 "dotnet watch",
468 "dotnet ef",
469 "mix phx.server",
471 "iex -s mix",
472 "swift run",
474 "swift package ",
475 "vapor serve",
476 "zig build run",
478 "cargo watch",
480 "cargo run",
481 "cargo leptos watch",
482 "bacon ",
483 "make dev",
485 "make serve",
486 "make watch",
487 "make run",
488 "make start",
489 "just dev",
490 "just serve",
491 "just watch",
492 "just start",
493 "just run",
494 "task dev",
495 "task serve",
496 "task watch",
497 "nix develop",
498 "devenv up",
499 "act ",
501 "skaffold dev",
502 "tilt up",
503 "garden dev",
504 "telepresence ",
505 "ab ",
507 "wrk ",
508 "hey ",
509 "vegeta ",
510 "k6 run",
511 "artillery run",
512 "az login",
514 "az account",
515 "gh auth",
516 "gcloud auth",
517 "gcloud init",
518 "aws sso",
519 "aws configure sso",
520 "firebase login",
521 "netlify login",
522 "vercel login",
523 "heroku login",
524 "flyctl auth",
525 "fly auth",
526 "railway login",
527 "supabase login",
528 "wrangler login",
529 "doppler login",
530 "vault login",
531 "oc login",
532 "kubelogin",
533 "--use-device-code",
534];
535
536const SCRIPT_RUNNER_PREFIXES: &[&str] = &[
537 "npm run ",
538 "npm start",
539 "npx ",
540 "pnpm run ",
541 "pnpm dev",
542 "pnpm start",
543 "pnpm preview",
544 "yarn ",
545 "bun run ",
546 "bun start",
547 "deno task ",
548];
549
550const DEV_SCRIPT_KEYWORDS: &[&str] = &[
551 "dev",
552 "start",
553 "serve",
554 "watch",
555 "preview",
556 "storybook",
557 "hot",
558 "live",
559 "hmr",
560];
561
562fn is_dev_script_runner(cmd: &str) -> bool {
563 for prefix in SCRIPT_RUNNER_PREFIXES {
564 if let Some(rest) = cmd.strip_prefix(prefix) {
565 let script_name = rest.split_whitespace().next().unwrap_or("");
566 for kw in DEV_SCRIPT_KEYWORDS {
567 if script_name.contains(kw) {
568 return true;
569 }
570 }
571 }
572 }
573 false
574}
575
576fn is_excluded_command(command: &str, excluded: &[String]) -> bool {
577 let cmd = command.trim().to_lowercase();
578 for pattern in BUILTIN_PASSTHROUGH {
579 if pattern.starts_with("--") {
580 if cmd.contains(pattern) {
581 return true;
582 }
583 } else if pattern.ends_with(' ') || pattern.ends_with('\t') {
584 if cmd == pattern.trim() || cmd.starts_with(pattern) {
585 return true;
586 }
587 } else if cmd == *pattern
588 || cmd.starts_with(&format!("{pattern} "))
589 || cmd.starts_with(&format!("{pattern}\t"))
590 || cmd.contains(&format!(" {pattern} "))
591 || cmd.contains(&format!(" {pattern}\t"))
592 || cmd.contains(&format!("|{pattern} "))
593 || cmd.contains(&format!("|{pattern}\t"))
594 || cmd.ends_with(&format!(" {pattern}"))
595 || cmd.ends_with(&format!("|{pattern}"))
596 {
597 return true;
598 }
599 }
600
601 if is_dev_script_runner(&cmd) {
602 return true;
603 }
604
605 if excluded.is_empty() {
606 return false;
607 }
608 excluded.iter().any(|excl| {
609 let excl_lower = excl.trim().to_lowercase();
610 cmd == excl_lower || cmd.starts_with(&format!("{excl_lower} "))
611 })
612}
613
614pub fn interactive() {
615 let real_shell = detect_shell();
616
617 eprintln!(
618 "lean-ctx shell v{} (wrapping {real_shell})",
619 env!("CARGO_PKG_VERSION")
620 );
621 eprintln!("All command output is automatically compressed.");
622 eprintln!("Type 'exit' to quit.\n");
623
624 let stdin = io::stdin();
625 let mut stdout = io::stdout();
626
627 loop {
628 let _ = write!(stdout, "lean-ctx> ");
629 let _ = stdout.flush();
630
631 let mut line = String::new();
632 match stdin.lock().read_line(&mut line) {
633 Ok(0) => break,
634 Ok(_) => {}
635 Err(_) => break,
636 }
637
638 let cmd = line.trim();
639 if cmd.is_empty() {
640 continue;
641 }
642 if cmd == "exit" || cmd == "quit" {
643 break;
644 }
645 if cmd == "gain" {
646 println!("{}", stats::format_gain());
647 continue;
648 }
649
650 let exit_code = exec(cmd);
651
652 if exit_code != 0 {
653 let _ = writeln!(stdout, "[exit: {exit_code}]");
654 }
655 }
656}
657
658fn compress_and_measure(command: &str, stdout: &str, stderr: &str) -> (String, usize) {
659 let compressed_stdout = compress_if_beneficial(command, stdout);
660 let compressed_stderr = compress_if_beneficial(command, stderr);
661
662 let mut result = String::new();
663 if !compressed_stdout.is_empty() {
664 result.push_str(&compressed_stdout);
665 }
666 if !compressed_stderr.is_empty() {
667 if !result.is_empty() {
668 result.push('\n');
669 }
670 result.push_str(&compressed_stderr);
671 }
672
673 let content_for_counting = if let Some(pos) = result.rfind("\n[lean-ctx: ") {
676 &result[..pos]
677 } else {
678 &result
679 };
680 let output_tokens = count_tokens(content_for_counting);
681 (result, output_tokens)
682}
683
684fn compress_if_beneficial(command: &str, output: &str) -> String {
685 if output.trim().is_empty() {
686 return String::new();
687 }
688
689 if crate::tools::ctx_shell::contains_auth_flow(output) {
690 return output.to_string();
691 }
692
693 let original_tokens = count_tokens(output);
694
695 if original_tokens < 50 {
696 return output.to_string();
697 }
698
699 let min_output_tokens = 5;
700
701 if let Some(compressed) = patterns::compress_output(command, output) {
702 if !compressed.trim().is_empty() {
703 let compressed_tokens = count_tokens(&compressed);
704 if compressed_tokens >= min_output_tokens && compressed_tokens < original_tokens {
705 let ratio = compressed_tokens as f64 / original_tokens as f64;
706 if ratio < 0.05 && original_tokens > 100 {
707 eprintln!(
708 "[lean-ctx] WARNING: compression removed >95% of content, returning original"
709 );
710 return output.to_string();
711 }
712 let saved = original_tokens - compressed_tokens;
713 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
714 if pct >= 5 {
715 return format!(
716 "{compressed}\n[lean-ctx: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
717 );
718 }
719 return compressed;
720 }
721 if compressed_tokens < min_output_tokens {
722 return output.to_string();
723 }
724 }
725 }
726
727 let cleaned = crate::core::compressor::lightweight_cleanup(output);
728 let cleaned_tokens = count_tokens(&cleaned);
729 if cleaned_tokens < original_tokens {
730 let lines: Vec<&str> = cleaned.lines().collect();
731 if lines.len() > 30 {
732 let compressed = truncate_with_safety_scan(&lines, original_tokens);
733 if let Some(c) = compressed {
734 return c;
735 }
736 }
737 if cleaned_tokens < original_tokens {
738 let saved = original_tokens - cleaned_tokens;
739 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
740 if pct >= 5 {
741 return format!(
742 "{cleaned}\n[lean-ctx: {original_tokens}→{cleaned_tokens} tok, -{pct}%]"
743 );
744 }
745 return cleaned;
746 }
747 }
748
749 let lines: Vec<&str> = output.lines().collect();
750 if lines.len() > 30 {
751 if let Some(c) = truncate_with_safety_scan(&lines, original_tokens) {
752 return c;
753 }
754 }
755
756 output.to_string()
757}
758
759fn truncate_with_safety_scan(lines: &[&str], original_tokens: usize) -> Option<String> {
760 use crate::core::safety_needles;
761
762 let first = &lines[..5];
763 let last = &lines[lines.len() - 5..];
764 let middle = &lines[5..lines.len() - 5];
765
766 let safety_lines = safety_needles::extract_safety_lines(middle, 20);
767 let safety_count = safety_lines.len();
768 let omitted = middle.len() - safety_count;
769
770 let mut parts = Vec::new();
771 parts.push(first.join("\n"));
772 if safety_count > 0 {
773 parts.push(format!(
774 "[{omitted} lines omitted, {safety_count} safety-relevant lines preserved]"
775 ));
776 parts.push(safety_lines.join("\n"));
777 } else {
778 parts.push(format!("[{omitted} lines omitted]"));
779 }
780 parts.push(last.join("\n"));
781
782 let compressed = parts.join("\n");
783 let ct = count_tokens(&compressed);
784 if ct >= original_tokens {
785 return None;
786 }
787 let saved = original_tokens - ct;
788 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
789 if pct >= 5 {
790 Some(format!(
791 "{compressed}\n[lean-ctx: {original_tokens}→{ct} tok, -{pct}%]"
792 ))
793 } else {
794 Some(compressed)
795 }
796}
797
798fn windows_shell_flag_for_exe_basename(exe_basename: &str) -> &'static str {
801 if exe_basename.contains("powershell") || exe_basename.contains("pwsh") {
802 "-Command"
803 } else if exe_basename == "cmd.exe" || exe_basename == "cmd" {
804 "/C"
805 } else {
806 "-c"
810 }
811}
812
813pub fn shell_and_flag() -> (String, String) {
814 let shell = detect_shell();
815 let flag = if cfg!(windows) {
816 let name = std::path::Path::new(&shell)
817 .file_name()
818 .and_then(|n| n.to_str())
819 .unwrap_or("")
820 .to_ascii_lowercase();
821 windows_shell_flag_for_exe_basename(&name).to_string()
822 } else {
823 "-c".to_string()
824 };
825 (shell, flag)
826}
827
828fn detect_shell() -> String {
829 if let Ok(shell) = std::env::var("LEAN_CTX_SHELL") {
830 return shell;
831 }
832
833 if let Ok(shell) = std::env::var("SHELL") {
834 let bin = std::path::Path::new(&shell)
835 .file_name()
836 .and_then(|n| n.to_str())
837 .unwrap_or("sh");
838
839 if bin == "lean-ctx" {
840 return find_real_shell();
841 }
842 return shell;
843 }
844
845 find_real_shell()
846}
847
848#[cfg(unix)]
849fn find_real_shell() -> String {
850 for shell in &["/bin/zsh", "/bin/bash", "/bin/sh"] {
851 if std::path::Path::new(shell).exists() {
852 return shell.to_string();
853 }
854 }
855 "/bin/sh".to_string()
856}
857
858#[cfg(windows)]
859fn find_real_shell() -> String {
860 if is_running_in_powershell() {
861 if let Ok(pwsh) = which_powershell() {
862 return pwsh;
863 }
864 }
865 if let Ok(comspec) = std::env::var("COMSPEC") {
866 return comspec;
867 }
868 "cmd.exe".to_string()
869}
870
871#[cfg(windows)]
872fn is_running_in_powershell() -> bool {
873 std::env::var("PSModulePath").is_ok()
874}
875
876#[cfg(windows)]
877fn which_powershell() -> Result<String, ()> {
878 for candidate in &["pwsh.exe", "powershell.exe"] {
879 if let Ok(output) = std::process::Command::new("where").arg(candidate).output() {
880 if output.status.success() {
881 if let Ok(path) = String::from_utf8(output.stdout) {
882 if let Some(first_line) = path.lines().next() {
883 let trimmed = first_line.trim();
884 if !trimmed.is_empty() {
885 return Ok(trimmed.to_string());
886 }
887 }
888 }
889 }
890 }
891 }
892 Err(())
893}
894
895pub fn save_tee(command: &str, output: &str) -> Option<String> {
896 let tee_dir = dirs::home_dir()?.join(".lean-ctx").join("tee");
897 std::fs::create_dir_all(&tee_dir).ok()?;
898
899 cleanup_old_tee_logs(&tee_dir);
900
901 let cmd_slug: String = command
902 .chars()
903 .take(40)
904 .map(|c| {
905 if c.is_alphanumeric() || c == '-' {
906 c
907 } else {
908 '_'
909 }
910 })
911 .collect();
912 let ts = chrono::Local::now().format("%Y-%m-%d_%H%M%S");
913 let filename = format!("{ts}_{cmd_slug}.log");
914 let path = tee_dir.join(&filename);
915
916 let masked = mask_sensitive_data(output);
917 std::fs::write(&path, masked).ok()?;
918 Some(path.to_string_lossy().to_string())
919}
920
921fn mask_sensitive_data(input: &str) -> String {
922 use regex::Regex;
923
924 let patterns: Vec<(&str, Regex)> = vec![
925 ("Bearer token", Regex::new(r"(?i)(bearer\s+)[a-zA-Z0-9\-_\.]{8,}").unwrap()),
926 ("Authorization header", Regex::new(r"(?i)(authorization:\s*(?:basic|bearer|token)\s+)[^\s\r\n]+").unwrap()),
927 ("API key param", Regex::new(r#"(?i)((?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|token|password|passwd|pwd|secret)\s*[=:]\s*)[^\s\r\n,;&"']+"#).unwrap()),
928 ("AWS key", Regex::new(r"(AKIA[0-9A-Z]{12,})").unwrap()),
929 ("Private key block", Regex::new(r"(?s)(-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----).+?(-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----)").unwrap()),
930 ("GitHub token", Regex::new(r"(gh[pousr]_)[a-zA-Z0-9]{20,}").unwrap()),
931 ("Generic long hex/base64 secret", Regex::new(r#"(?i)(?:key|token|secret|password|credential|auth)\s*[=:]\s*['"]?([a-zA-Z0-9+/=\-_]{32,})['"]?"#).unwrap()),
932 ];
933
934 let mut result = input.to_string();
935 for (label, re) in &patterns {
936 result = re
937 .replace_all(&result, |caps: ®ex::Captures| {
938 if let Some(prefix) = caps.get(1) {
939 format!("{}[REDACTED:{}]", prefix.as_str(), label)
940 } else {
941 format!("[REDACTED:{}]", label)
942 }
943 })
944 .to_string();
945 }
946 result
947}
948
949fn cleanup_old_tee_logs(tee_dir: &std::path::Path) {
950 let cutoff =
951 std::time::SystemTime::now().checked_sub(std::time::Duration::from_secs(24 * 60 * 60));
952 let cutoff = match cutoff {
953 Some(t) => t,
954 None => return,
955 };
956
957 if let Ok(entries) = std::fs::read_dir(tee_dir) {
958 for entry in entries.flatten() {
959 if let Ok(meta) = entry.metadata() {
960 if let Ok(modified) = meta.modified() {
961 if modified < cutoff {
962 let _ = std::fs::remove_file(entry.path());
963 }
964 }
965 }
966 }
967 }
968}
969
970pub fn join_command(args: &[String]) -> String {
977 let (_, flag) = shell_and_flag();
978 join_command_for(args, &flag)
979}
980
981fn join_command_for(args: &[String], shell_flag: &str) -> String {
982 match shell_flag {
983 "-Command" => join_powershell(args),
984 "/C" => join_cmd(args),
985 _ => join_posix(args),
986 }
987}
988
989fn join_posix(args: &[String]) -> String {
990 args.iter()
991 .map(|a| quote_posix(a))
992 .collect::<Vec<_>>()
993 .join(" ")
994}
995
996fn join_powershell(args: &[String]) -> String {
997 let quoted: Vec<String> = args.iter().map(|a| quote_powershell(a)).collect();
998 format!("& {}", quoted.join(" "))
999}
1000
1001fn join_cmd(args: &[String]) -> String {
1002 args.iter()
1003 .map(|a| quote_cmd(a))
1004 .collect::<Vec<_>>()
1005 .join(" ")
1006}
1007
1008fn quote_posix(s: &str) -> String {
1009 if s.is_empty() {
1010 return "''".to_string();
1011 }
1012 if s.bytes()
1013 .all(|b| b.is_ascii_alphanumeric() || b"-_./=:@,+%^".contains(&b))
1014 {
1015 return s.to_string();
1016 }
1017 format!("'{}'", s.replace('\'', "'\\''"))
1018}
1019
1020fn quote_powershell(s: &str) -> String {
1021 if s.is_empty() {
1022 return "''".to_string();
1023 }
1024 if s.bytes()
1025 .all(|b| b.is_ascii_alphanumeric() || b"-_./=:@,+%^".contains(&b))
1026 {
1027 return s.to_string();
1028 }
1029 format!("'{}'", s.replace('\'', "''"))
1030}
1031
1032fn quote_cmd(s: &str) -> String {
1033 if s.is_empty() {
1034 return "\"\"".to_string();
1035 }
1036 if s.bytes()
1037 .all(|b| b.is_ascii_alphanumeric() || b"-_./=:@,+%^\\".contains(&b))
1038 {
1039 return s.to_string();
1040 }
1041 format!("\"{}\"", s.replace('"', "\\\""))
1042}
1043
1044#[cfg(test)]
1045mod join_command_tests {
1046 use super::*;
1047
1048 #[test]
1049 fn posix_simple_args() {
1050 let args: Vec<String> = vec!["git".into(), "status".into()];
1051 assert_eq!(join_command_for(&args, "-c"), "git status");
1052 }
1053
1054 #[test]
1055 fn posix_path_with_spaces() {
1056 let args: Vec<String> = vec!["/usr/local/my app/bin".into(), "--help".into()];
1057 assert_eq!(
1058 join_command_for(&args, "-c"),
1059 "'/usr/local/my app/bin' --help"
1060 );
1061 }
1062
1063 #[test]
1064 fn posix_single_quotes_escaped() {
1065 let args: Vec<String> = vec!["echo".into(), "it's".into()];
1066 assert_eq!(join_command_for(&args, "-c"), "echo 'it'\\''s'");
1067 }
1068
1069 #[test]
1070 fn posix_empty_arg() {
1071 let args: Vec<String> = vec!["cmd".into(), "".into()];
1072 assert_eq!(join_command_for(&args, "-c"), "cmd ''");
1073 }
1074
1075 #[test]
1076 fn powershell_simple_args() {
1077 let args: Vec<String> = vec!["npm".into(), "install".into()];
1078 assert_eq!(join_command_for(&args, "-Command"), "& npm install");
1079 }
1080
1081 #[test]
1082 fn powershell_path_with_spaces() {
1083 let args: Vec<String> = vec![
1084 "C:\\Program Files\\nodejs\\npm.cmd".into(),
1085 "install".into(),
1086 ];
1087 assert_eq!(
1088 join_command_for(&args, "-Command"),
1089 "& 'C:\\Program Files\\nodejs\\npm.cmd' install"
1090 );
1091 }
1092
1093 #[test]
1094 fn powershell_single_quotes_escaped() {
1095 let args: Vec<String> = vec!["echo".into(), "it's done".into()];
1096 assert_eq!(join_command_for(&args, "-Command"), "& echo 'it''s done'");
1097 }
1098
1099 #[test]
1100 fn cmd_simple_args() {
1101 let args: Vec<String> = vec!["npm.cmd".into(), "install".into()];
1102 assert_eq!(join_command_for(&args, "/C"), "npm.cmd install");
1103 }
1104
1105 #[test]
1106 fn cmd_path_with_spaces() {
1107 let args: Vec<String> = vec![
1108 "C:\\Program Files\\nodejs\\npm.cmd".into(),
1109 "install".into(),
1110 ];
1111 assert_eq!(
1112 join_command_for(&args, "/C"),
1113 "\"C:\\Program Files\\nodejs\\npm.cmd\" install"
1114 );
1115 }
1116
1117 #[test]
1118 fn cmd_double_quotes_escaped() {
1119 let args: Vec<String> = vec!["echo".into(), "say \"hello\"".into()];
1120 assert_eq!(join_command_for(&args, "/C"), "echo \"say \\\"hello\\\"\"");
1121 }
1122
1123 #[test]
1124 fn unknown_flag_uses_posix() {
1125 let args: Vec<String> = vec!["ls".into(), "-la".into()];
1126 assert_eq!(join_command_for(&args, "--exec"), "ls -la");
1127 }
1128}
1129
1130#[cfg(test)]
1131mod windows_shell_flag_tests {
1132 use super::windows_shell_flag_for_exe_basename;
1133
1134 #[test]
1135 fn cmd_uses_slash_c() {
1136 assert_eq!(windows_shell_flag_for_exe_basename("cmd.exe"), "/C");
1137 assert_eq!(windows_shell_flag_for_exe_basename("cmd"), "/C");
1138 }
1139
1140 #[test]
1141 fn powershell_uses_command() {
1142 assert_eq!(
1143 windows_shell_flag_for_exe_basename("powershell.exe"),
1144 "-Command"
1145 );
1146 assert_eq!(windows_shell_flag_for_exe_basename("pwsh.exe"), "-Command");
1147 }
1148
1149 #[test]
1150 fn posix_shells_use_dash_c() {
1151 assert_eq!(windows_shell_flag_for_exe_basename("bash.exe"), "-c");
1152 assert_eq!(windows_shell_flag_for_exe_basename("bash"), "-c");
1153 assert_eq!(windows_shell_flag_for_exe_basename("sh.exe"), "-c");
1154 assert_eq!(windows_shell_flag_for_exe_basename("zsh.exe"), "-c");
1155 assert_eq!(windows_shell_flag_for_exe_basename("fish.exe"), "-c");
1156 }
1157}
1158
1159#[cfg(test)]
1160mod passthrough_tests {
1161 use super::is_excluded_command;
1162
1163 #[test]
1164 fn turbo_is_passthrough() {
1165 assert!(is_excluded_command("turbo run dev", &[]));
1166 assert!(is_excluded_command("turbo run build", &[]));
1167 assert!(is_excluded_command("pnpm turbo run dev", &[]));
1168 assert!(is_excluded_command("npx turbo run dev", &[]));
1169 }
1170
1171 #[test]
1172 fn dev_servers_are_passthrough() {
1173 assert!(is_excluded_command("next dev", &[]));
1174 assert!(is_excluded_command("vite dev", &[]));
1175 assert!(is_excluded_command("nuxt dev", &[]));
1176 assert!(is_excluded_command("astro dev", &[]));
1177 assert!(is_excluded_command("nodemon server.js", &[]));
1178 }
1179
1180 #[test]
1181 fn interactive_tools_are_passthrough() {
1182 assert!(is_excluded_command("vim file.rs", &[]));
1183 assert!(is_excluded_command("nvim", &[]));
1184 assert!(is_excluded_command("htop", &[]));
1185 assert!(is_excluded_command("ssh user@host", &[]));
1186 assert!(is_excluded_command("tail -f /var/log/syslog", &[]));
1187 }
1188
1189 #[test]
1190 fn docker_streaming_is_passthrough() {
1191 assert!(is_excluded_command("docker logs my-container", &[]));
1192 assert!(is_excluded_command("docker logs -f webapp", &[]));
1193 assert!(is_excluded_command("docker attach my-container", &[]));
1194 assert!(is_excluded_command("docker exec -it web bash", &[]));
1195 assert!(is_excluded_command("docker exec -ti web bash", &[]));
1196 assert!(is_excluded_command("docker run -it ubuntu bash", &[]));
1197 assert!(is_excluded_command("docker compose exec web bash", &[]));
1198 assert!(is_excluded_command("docker stats", &[]));
1199 assert!(is_excluded_command("docker events", &[]));
1200 }
1201
1202 #[test]
1203 fn kubectl_is_passthrough() {
1204 assert!(is_excluded_command("kubectl logs my-pod", &[]));
1205 assert!(is_excluded_command("kubectl logs -f deploy/web", &[]));
1206 assert!(is_excluded_command("kubectl exec -it pod -- bash", &[]));
1207 assert!(is_excluded_command(
1208 "kubectl port-forward svc/web 8080:80",
1209 &[]
1210 ));
1211 assert!(is_excluded_command("kubectl attach my-pod", &[]));
1212 assert!(is_excluded_command("kubectl proxy", &[]));
1213 }
1214
1215 #[test]
1216 fn database_repls_are_passthrough() {
1217 assert!(is_excluded_command("psql -U user mydb", &[]));
1218 assert!(is_excluded_command("mysql -u root -p", &[]));
1219 assert!(is_excluded_command("sqlite3 data.db", &[]));
1220 assert!(is_excluded_command("redis-cli", &[]));
1221 assert!(is_excluded_command("mongosh", &[]));
1222 }
1223
1224 #[test]
1225 fn streaming_tools_are_passthrough() {
1226 assert!(is_excluded_command("journalctl -f", &[]));
1227 assert!(is_excluded_command("ping 8.8.8.8", &[]));
1228 assert!(is_excluded_command("strace -p 1234", &[]));
1229 assert!(is_excluded_command("tcpdump -i eth0", &[]));
1230 assert!(is_excluded_command("tail -F /var/log/app.log", &[]));
1231 assert!(is_excluded_command("tmux new -s work", &[]));
1232 assert!(is_excluded_command("screen -S dev", &[]));
1233 }
1234
1235 #[test]
1236 fn additional_dev_servers_are_passthrough() {
1237 assert!(is_excluded_command("gatsby develop", &[]));
1238 assert!(is_excluded_command("ng serve --port 4200", &[]));
1239 assert!(is_excluded_command("remix dev", &[]));
1240 assert!(is_excluded_command("wrangler dev", &[]));
1241 assert!(is_excluded_command("hugo server", &[]));
1242 assert!(is_excluded_command("bun dev", &[]));
1243 assert!(is_excluded_command("cargo watch -x test", &[]));
1244 }
1245
1246 #[test]
1247 fn normal_commands_not_excluded() {
1248 assert!(!is_excluded_command("git status", &[]));
1249 assert!(!is_excluded_command("cargo test", &[]));
1250 assert!(!is_excluded_command("npm run build", &[]));
1251 assert!(!is_excluded_command("ls -la", &[]));
1252 }
1253
1254 #[test]
1255 fn user_exclusions_work() {
1256 let excl = vec!["myapp".to_string()];
1257 assert!(is_excluded_command("myapp serve", &excl));
1258 assert!(!is_excluded_command("git status", &excl));
1259 }
1260
1261 #[test]
1262 fn is_container_returns_bool() {
1263 let _ = super::is_container();
1264 }
1265
1266 #[test]
1267 fn is_non_interactive_returns_bool() {
1268 let _ = super::is_non_interactive();
1269 }
1270
1271 #[test]
1272 fn auth_commands_excluded() {
1273 assert!(is_excluded_command("az login --use-device-code", &[]));
1274 assert!(is_excluded_command("gh auth login", &[]));
1275 assert!(is_excluded_command("gcloud auth login", &[]));
1276 assert!(is_excluded_command("aws sso login", &[]));
1277 assert!(is_excluded_command("firebase login", &[]));
1278 assert!(is_excluded_command("vercel login", &[]));
1279 assert!(is_excluded_command("heroku login", &[]));
1280 assert!(is_excluded_command("az login", &[]));
1281 assert!(is_excluded_command("kubelogin convert-kubeconfig", &[]));
1282 assert!(is_excluded_command("vault login -method=oidc", &[]));
1283 assert!(is_excluded_command("flyctl auth login", &[]));
1284 }
1285
1286 #[test]
1287 fn auth_exclusion_does_not_affect_normal_commands() {
1288 assert!(!is_excluded_command("git log", &[]));
1289 assert!(!is_excluded_command("npm run build", &[]));
1290 assert!(!is_excluded_command("cargo test", &[]));
1291 assert!(!is_excluded_command("aws s3 ls", &[]));
1292 assert!(!is_excluded_command("gcloud compute instances list", &[]));
1293 assert!(!is_excluded_command("az vm list", &[]));
1294 }
1295
1296 #[test]
1297 fn npm_script_runners_are_passthrough() {
1298 assert!(is_excluded_command("npm run dev", &[]));
1299 assert!(is_excluded_command("npm run start", &[]));
1300 assert!(is_excluded_command("npm run serve", &[]));
1301 assert!(is_excluded_command("npm run watch", &[]));
1302 assert!(is_excluded_command("npm run preview", &[]));
1303 assert!(is_excluded_command("npm run storybook", &[]));
1304 assert!(is_excluded_command("npm run test:watch", &[]));
1305 assert!(is_excluded_command("npm start", &[]));
1306 assert!(is_excluded_command("npx vite", &[]));
1307 assert!(is_excluded_command("npx next dev", &[]));
1308 }
1309
1310 #[test]
1311 fn pnpm_script_runners_are_passthrough() {
1312 assert!(is_excluded_command("pnpm run dev", &[]));
1313 assert!(is_excluded_command("pnpm run start", &[]));
1314 assert!(is_excluded_command("pnpm run serve", &[]));
1315 assert!(is_excluded_command("pnpm run watch", &[]));
1316 assert!(is_excluded_command("pnpm run preview", &[]));
1317 assert!(is_excluded_command("pnpm dev", &[]));
1318 assert!(is_excluded_command("pnpm start", &[]));
1319 assert!(is_excluded_command("pnpm preview", &[]));
1320 }
1321
1322 #[test]
1323 fn yarn_script_runners_are_passthrough() {
1324 assert!(is_excluded_command("yarn dev", &[]));
1325 assert!(is_excluded_command("yarn start", &[]));
1326 assert!(is_excluded_command("yarn serve", &[]));
1327 assert!(is_excluded_command("yarn watch", &[]));
1328 assert!(is_excluded_command("yarn preview", &[]));
1329 assert!(is_excluded_command("yarn storybook", &[]));
1330 }
1331
1332 #[test]
1333 fn bun_deno_script_runners_are_passthrough() {
1334 assert!(is_excluded_command("bun run dev", &[]));
1335 assert!(is_excluded_command("bun run start", &[]));
1336 assert!(is_excluded_command("bun run serve", &[]));
1337 assert!(is_excluded_command("bun run watch", &[]));
1338 assert!(is_excluded_command("bun run preview", &[]));
1339 assert!(is_excluded_command("bun start", &[]));
1340 assert!(is_excluded_command("deno task dev", &[]));
1341 assert!(is_excluded_command("deno task start", &[]));
1342 assert!(is_excluded_command("deno task serve", &[]));
1343 assert!(is_excluded_command("deno run --watch main.ts", &[]));
1344 }
1345
1346 #[test]
1347 fn python_servers_are_passthrough() {
1348 assert!(is_excluded_command("flask run --port 5000", &[]));
1349 assert!(is_excluded_command("uvicorn app:app --reload", &[]));
1350 assert!(is_excluded_command("gunicorn app:app -w 4", &[]));
1351 assert!(is_excluded_command("hypercorn app:app", &[]));
1352 assert!(is_excluded_command("daphne app.asgi:application", &[]));
1353 assert!(is_excluded_command(
1354 "django-admin runserver 0.0.0.0:8000",
1355 &[]
1356 ));
1357 assert!(is_excluded_command("python manage.py runserver", &[]));
1358 assert!(is_excluded_command("python -m http.server 8080", &[]));
1359 assert!(is_excluded_command("python3 -m http.server", &[]));
1360 assert!(is_excluded_command("streamlit run app.py", &[]));
1361 assert!(is_excluded_command("gradio app.py", &[]));
1362 assert!(is_excluded_command("celery worker -A app", &[]));
1363 assert!(is_excluded_command("celery -A app worker", &[]));
1364 assert!(is_excluded_command("celery -B", &[]));
1365 assert!(is_excluded_command("dramatiq tasks", &[]));
1366 assert!(is_excluded_command("rq worker", &[]));
1367 assert!(is_excluded_command("ptw tests/", &[]));
1368 assert!(is_excluded_command("pytest-watch", &[]));
1369 }
1370
1371 #[test]
1372 fn ruby_servers_are_passthrough() {
1373 assert!(is_excluded_command("rails server -p 3000", &[]));
1374 assert!(is_excluded_command("rails s", &[]));
1375 assert!(is_excluded_command("puma -C config.rb", &[]));
1376 assert!(is_excluded_command("unicorn -c config.rb", &[]));
1377 assert!(is_excluded_command("thin start", &[]));
1378 assert!(is_excluded_command("foreman start", &[]));
1379 assert!(is_excluded_command("overmind start", &[]));
1380 assert!(is_excluded_command("guard -G Guardfile", &[]));
1381 assert!(is_excluded_command("sidekiq", &[]));
1382 assert!(is_excluded_command("resque work", &[]));
1383 }
1384
1385 #[test]
1386 fn php_servers_are_passthrough() {
1387 assert!(is_excluded_command("php artisan serve", &[]));
1388 assert!(is_excluded_command("php -S localhost:8000", &[]));
1389 assert!(is_excluded_command("php artisan queue:work", &[]));
1390 assert!(is_excluded_command("php artisan queue:listen", &[]));
1391 assert!(is_excluded_command("php artisan horizon", &[]));
1392 assert!(is_excluded_command("php artisan tinker", &[]));
1393 assert!(is_excluded_command("sail up", &[]));
1394 }
1395
1396 #[test]
1397 fn java_servers_are_passthrough() {
1398 assert!(is_excluded_command("./gradlew bootRun", &[]));
1399 assert!(is_excluded_command("gradlew bootRun", &[]));
1400 assert!(is_excluded_command("gradle bootRun", &[]));
1401 assert!(is_excluded_command("mvn spring-boot:run", &[]));
1402 assert!(is_excluded_command("./mvnw spring-boot:run", &[]));
1403 assert!(is_excluded_command("mvn quarkus:dev", &[]));
1404 assert!(is_excluded_command("./mvnw quarkus:dev", &[]));
1405 assert!(is_excluded_command("sbt run", &[]));
1406 assert!(is_excluded_command("sbt ~compile", &[]));
1407 assert!(is_excluded_command("lein run", &[]));
1408 assert!(is_excluded_command("lein repl", &[]));
1409 assert!(is_excluded_command("./gradlew run", &[]));
1410 }
1411
1412 #[test]
1413 fn go_servers_are_passthrough() {
1414 assert!(is_excluded_command("go run main.go", &[]));
1415 assert!(is_excluded_command("go run ./cmd/server", &[]));
1416 assert!(is_excluded_command("air -c .air.toml", &[]));
1417 assert!(is_excluded_command("gin --port 3000", &[]));
1418 assert!(is_excluded_command("realize start", &[]));
1419 assert!(is_excluded_command("reflex -r '.go$' go run .", &[]));
1420 assert!(is_excluded_command("gowatch run", &[]));
1421 }
1422
1423 #[test]
1424 fn dotnet_servers_are_passthrough() {
1425 assert!(is_excluded_command("dotnet run", &[]));
1426 assert!(is_excluded_command("dotnet run --project src/Api", &[]));
1427 assert!(is_excluded_command("dotnet watch run", &[]));
1428 assert!(is_excluded_command("dotnet ef database update", &[]));
1429 }
1430
1431 #[test]
1432 fn elixir_servers_are_passthrough() {
1433 assert!(is_excluded_command("mix phx.server", &[]));
1434 assert!(is_excluded_command("iex -s mix phx.server", &[]));
1435 assert!(is_excluded_command("iex -S mix phx.server", &[]));
1436 }
1437
1438 #[test]
1439 fn swift_zig_servers_are_passthrough() {
1440 assert!(is_excluded_command("swift run MyApp", &[]));
1441 assert!(is_excluded_command("swift package resolve", &[]));
1442 assert!(is_excluded_command("vapor serve --port 8080", &[]));
1443 assert!(is_excluded_command("zig build run", &[]));
1444 }
1445
1446 #[test]
1447 fn rust_watchers_are_passthrough() {
1448 assert!(is_excluded_command("cargo watch -x test", &[]));
1449 assert!(is_excluded_command("cargo run --bin server", &[]));
1450 assert!(is_excluded_command("cargo leptos watch", &[]));
1451 assert!(is_excluded_command("bacon test", &[]));
1452 }
1453
1454 #[test]
1455 fn general_task_runners_are_passthrough() {
1456 assert!(is_excluded_command("make dev", &[]));
1457 assert!(is_excluded_command("make serve", &[]));
1458 assert!(is_excluded_command("make watch", &[]));
1459 assert!(is_excluded_command("make run", &[]));
1460 assert!(is_excluded_command("make start", &[]));
1461 assert!(is_excluded_command("just dev", &[]));
1462 assert!(is_excluded_command("just serve", &[]));
1463 assert!(is_excluded_command("just watch", &[]));
1464 assert!(is_excluded_command("just start", &[]));
1465 assert!(is_excluded_command("just run", &[]));
1466 assert!(is_excluded_command("task dev", &[]));
1467 assert!(is_excluded_command("task serve", &[]));
1468 assert!(is_excluded_command("task watch", &[]));
1469 assert!(is_excluded_command("nix develop", &[]));
1470 assert!(is_excluded_command("devenv up", &[]));
1471 }
1472
1473 #[test]
1474 fn cicd_infra_are_passthrough() {
1475 assert!(is_excluded_command("act push", &[]));
1476 assert!(is_excluded_command("docker compose watch", &[]));
1477 assert!(is_excluded_command("docker-compose watch", &[]));
1478 assert!(is_excluded_command("skaffold dev", &[]));
1479 assert!(is_excluded_command("tilt up", &[]));
1480 assert!(is_excluded_command("garden dev", &[]));
1481 assert!(is_excluded_command("telepresence connect", &[]));
1482 }
1483
1484 #[test]
1485 fn networking_monitoring_are_passthrough() {
1486 assert!(is_excluded_command("mtr 8.8.8.8", &[]));
1487 assert!(is_excluded_command("nmap -sV host", &[]));
1488 assert!(is_excluded_command("iperf -s", &[]));
1489 assert!(is_excluded_command("iperf3 -c host", &[]));
1490 assert!(is_excluded_command("socat TCP-LISTEN:8080,fork -", &[]));
1491 }
1492
1493 #[test]
1494 fn load_testing_is_passthrough() {
1495 assert!(is_excluded_command("ab -n 1000 http://localhost/", &[]));
1496 assert!(is_excluded_command("wrk -t12 -c400 http://localhost/", &[]));
1497 assert!(is_excluded_command("hey -n 10000 http://localhost/", &[]));
1498 assert!(is_excluded_command("vegeta attack", &[]));
1499 assert!(is_excluded_command("k6 run script.js", &[]));
1500 assert!(is_excluded_command("artillery run test.yml", &[]));
1501 }
1502
1503 #[test]
1504 fn smart_script_detection_works() {
1505 assert!(is_excluded_command("npm run dev:ssr", &[]));
1506 assert!(is_excluded_command("npm run dev:local", &[]));
1507 assert!(is_excluded_command("yarn start:production", &[]));
1508 assert!(is_excluded_command("pnpm run serve:local", &[]));
1509 assert!(is_excluded_command("bun run watch:css", &[]));
1510 assert!(is_excluded_command("deno task dev:api", &[]));
1511 assert!(is_excluded_command("npm run storybook:ci", &[]));
1512 assert!(is_excluded_command("yarn preview:staging", &[]));
1513 assert!(is_excluded_command("pnpm run hot-reload", &[]));
1514 assert!(is_excluded_command("npm run hmr-server", &[]));
1515 assert!(is_excluded_command("bun run live-server", &[]));
1516 }
1517
1518 #[test]
1519 fn smart_detection_does_not_false_positive() {
1520 assert!(!is_excluded_command("npm run build", &[]));
1521 assert!(!is_excluded_command("npm run lint", &[]));
1522 assert!(!is_excluded_command("npm run test", &[]));
1523 assert!(!is_excluded_command("npm run format", &[]));
1524 assert!(!is_excluded_command("yarn build", &[]));
1525 assert!(!is_excluded_command("yarn test", &[]));
1526 assert!(!is_excluded_command("pnpm run lint", &[]));
1527 assert!(!is_excluded_command("bun run build", &[]));
1528 }
1529}
1530
1531pub fn compress_if_beneficial_pub(command: &str, output: &str) -> String {
1533 compress_if_beneficial(command, output)
1534}