1use crate::core::patterns;
2use crate::core::tokens::count_tokens;
3
4const BUILTIN_PASSTHROUGH: &[&str] = &[
5 "turbo",
7 "nx serve",
8 "nx dev",
9 "next dev",
10 "vite dev",
11 "vite preview",
12 "vitest",
13 "nuxt dev",
14 "astro dev",
15 "webpack serve",
16 "webpack-dev-server",
17 "nodemon",
18 "concurrently",
19 "pm2",
20 "pm2 logs",
21 "gatsby develop",
22 "expo start",
23 "react-scripts start",
24 "ng serve",
25 "remix dev",
26 "wrangler dev",
27 "hugo server",
28 "hugo serve",
29 "jekyll serve",
30 "bun dev",
31 "ember serve",
32 "npm run dev",
34 "npm run start",
35 "npm run serve",
36 "npm run watch",
37 "npm run preview",
38 "npm run storybook",
39 "npm run test:watch",
40 "npm start",
41 "npx ",
42 "pnpm run dev",
43 "pnpm run start",
44 "pnpm run serve",
45 "pnpm run watch",
46 "pnpm run preview",
47 "pnpm run storybook",
48 "pnpm dev",
49 "pnpm start",
50 "pnpm preview",
51 "yarn dev",
52 "yarn start",
53 "yarn serve",
54 "yarn watch",
55 "yarn preview",
56 "yarn storybook",
57 "bun run dev",
58 "bun run start",
59 "bun run serve",
60 "bun run watch",
61 "bun run preview",
62 "bun start",
63 "deno task dev",
64 "deno task start",
65 "deno task serve",
66 "deno run --watch",
67 "docker compose up",
69 "docker-compose up",
70 "docker compose logs",
71 "docker-compose logs",
72 "docker compose exec",
73 "docker-compose exec",
74 "docker compose run",
75 "docker-compose run",
76 "docker compose watch",
77 "docker-compose watch",
78 "docker logs",
79 "docker attach",
80 "docker exec -it",
81 "docker exec -ti",
82 "docker run -it",
83 "docker run -ti",
84 "docker stats",
85 "docker events",
86 "kubectl logs",
88 "kubectl exec -it",
89 "kubectl exec -ti",
90 "kubectl attach",
91 "kubectl port-forward",
92 "kubectl proxy",
93 "top",
95 "htop",
96 "btop",
97 "watch ",
98 "tail -f",
99 "tail -f ",
100 "journalctl -f",
101 "journalctl --follow",
102 "dmesg -w",
103 "dmesg --follow",
104 "strace",
105 "tcpdump",
106 "ping ",
107 "ping6 ",
108 "traceroute",
109 "mtr ",
110 "nmap ",
111 "iperf ",
112 "iperf3 ",
113 "ss -l",
114 "netstat -l",
115 "lsof -i",
116 "socat ",
117 "less",
119 "more",
120 "vim",
121 "nvim",
122 "vi ",
123 "nano",
124 "micro ",
125 "helix ",
126 "hx ",
127 "emacs",
128 "tmux",
130 "screen",
131 "ssh ",
133 "telnet ",
134 "nc ",
135 "ncat ",
136 "psql",
137 "mysql",
138 "sqlite3",
139 "redis-cli",
140 "mongosh",
141 "mongo ",
142 "python3 -i",
143 "python -i",
144 "irb",
145 "rails console",
146 "rails c ",
147 "iex",
148 "flask run",
150 "uvicorn ",
151 "gunicorn ",
152 "hypercorn ",
153 "daphne ",
154 "django-admin runserver",
155 "manage.py runserver",
156 "python manage.py runserver",
157 "python -m http.server",
158 "python3 -m http.server",
159 "streamlit run",
160 "gradio ",
161 "celery worker",
162 "celery -a",
163 "celery -b",
164 "dramatiq ",
165 "rq worker",
166 "watchmedo ",
167 "ptw ",
168 "pytest-watch",
169 "rails server",
171 "rails s",
172 "puma ",
173 "unicorn ",
174 "thin start",
175 "foreman start",
176 "overmind start",
177 "guard ",
178 "sidekiq",
179 "resque ",
180 "php artisan serve",
182 "php -s ",
183 "php artisan queue:work",
184 "php artisan queue:listen",
185 "php artisan horizon",
186 "php artisan tinker",
187 "sail up",
188 "./gradlew bootrun",
190 "gradlew bootrun",
191 "gradle bootrun",
192 "./gradlew run",
193 "mvn spring-boot:run",
194 "./mvnw spring-boot:run",
195 "mvnw spring-boot:run",
196 "mvn quarkus:dev",
197 "./mvnw quarkus:dev",
198 "sbt run",
199 "sbt ~compile",
200 "lein run",
201 "lein repl",
202 "go run ",
204 "air ",
205 "gin ",
206 "realize start",
207 "reflex ",
208 "gowatch ",
209 "dotnet run",
211 "dotnet watch",
212 "dotnet ef",
213 "mix phx.server",
215 "iex -s mix",
216 "swift run",
218 "swift package ",
219 "vapor serve",
220 "zig build run",
222 "cargo watch",
224 "cargo run",
225 "cargo leptos watch",
226 "bacon ",
227 "make dev",
229 "make serve",
230 "make watch",
231 "make run",
232 "make start",
233 "just dev",
234 "just serve",
235 "just watch",
236 "just start",
237 "just run",
238 "task dev",
239 "task serve",
240 "task watch",
241 "nix develop",
242 "devenv up",
243 "act ",
245 "skaffold dev",
246 "tilt up",
247 "garden dev",
248 "telepresence ",
249 "ab ",
251 "wrk ",
252 "hey ",
253 "vegeta ",
254 "k6 run",
255 "artillery run",
256 "az login",
258 "az account",
259 "gh",
260 "gcloud auth",
261 "gcloud init",
262 "aws sso",
263 "aws configure sso",
264 "firebase login",
265 "netlify login",
266 "vercel login",
267 "heroku login",
268 "flyctl auth",
269 "fly auth",
270 "railway login",
271 "supabase login",
272 "wrangler login",
273 "doppler login",
274 "vault login",
275 "oc login",
276 "kubelogin",
277 "--use-device-code",
278];
279
280const SCRIPT_RUNNER_PREFIXES: &[&str] = &[
281 "npm run ",
282 "npm start",
283 "npx ",
284 "pnpm run ",
285 "pnpm dev",
286 "pnpm start",
287 "pnpm preview",
288 "yarn ",
289 "bun run ",
290 "bun start",
291 "deno task ",
292];
293
294const DEV_SCRIPT_KEYWORDS: &[&str] = &[
295 "dev",
296 "start",
297 "serve",
298 "watch",
299 "preview",
300 "storybook",
301 "hot",
302 "live",
303 "hmr",
304];
305
306fn is_dev_script_runner(cmd: &str) -> bool {
307 for prefix in SCRIPT_RUNNER_PREFIXES {
308 if let Some(rest) = cmd.strip_prefix(prefix) {
309 let script_name = rest.split_whitespace().next().unwrap_or("");
310 for kw in DEV_SCRIPT_KEYWORDS {
311 if script_name.contains(kw) {
312 return true;
313 }
314 }
315 }
316 }
317 false
318}
319
320pub(super) fn is_excluded_command(command: &str, excluded: &[String]) -> bool {
321 let cmd = command.trim().to_lowercase();
322 for pattern in BUILTIN_PASSTHROUGH {
323 if pattern.starts_with("--") {
324 if cmd.contains(pattern) {
325 return true;
326 }
327 } else if pattern.ends_with(' ') || pattern.ends_with('\t') {
328 if cmd == pattern.trim() || cmd.starts_with(pattern) {
329 return true;
330 }
331 } else if cmd == *pattern
332 || cmd.starts_with(&format!("{pattern} "))
333 || cmd.starts_with(&format!("{pattern}\t"))
334 || cmd.contains(&format!(" {pattern} "))
335 || cmd.contains(&format!(" {pattern}\t"))
336 || cmd.contains(&format!("|{pattern} "))
337 || cmd.contains(&format!("|{pattern}\t"))
338 || cmd.ends_with(&format!(" {pattern}"))
339 || cmd.ends_with(&format!("|{pattern}"))
340 {
341 return true;
342 }
343 }
344
345 if is_dev_script_runner(&cmd) {
346 return true;
347 }
348
349 if excluded.is_empty() {
350 return false;
351 }
352 excluded.iter().any(|excl| {
353 let excl_lower = excl.trim().to_lowercase();
354 cmd == excl_lower || cmd.starts_with(&format!("{excl_lower} "))
355 })
356}
357
358pub(super) fn compress_and_measure(command: &str, stdout: &str, stderr: &str) -> (String, usize) {
359 let compressed_stdout = compress_if_beneficial(command, stdout);
360 let compressed_stderr = compress_if_beneficial(command, stderr);
361
362 let mut result = String::new();
363 if !compressed_stdout.is_empty() {
364 result.push_str(&compressed_stdout);
365 }
366 if !compressed_stderr.is_empty() {
367 if !result.is_empty() {
368 result.push('\n');
369 }
370 result.push_str(&compressed_stderr);
371 }
372
373 let content_for_counting = if let Some(pos) = result.rfind("\n[lean-ctx: ") {
374 &result[..pos]
375 } else {
376 &result
377 };
378 let output_tokens = count_tokens(content_for_counting);
379 (result, output_tokens)
380}
381
382fn is_search_output(command: &str) -> bool {
383 let c = command.trim_start();
384 c.starts_with("grep ")
385 || c.starts_with("rg ")
386 || c.starts_with("find ")
387 || c.starts_with("fd ")
388 || c.starts_with("ag ")
389 || c.starts_with("ack ")
390}
391
392pub fn has_structural_output(command: &str) -> bool {
398 if is_verbatim_output(command) {
399 return true;
400 }
401 if is_standalone_diff_command(command) {
402 return true;
403 }
404 is_structural_git_command(command)
405}
406
407pub fn is_verbatim_output(command: &str) -> bool {
411 is_verbatim_single(command) || is_verbatim_pipe_tail(command)
412}
413
414fn is_verbatim_single(command: &str) -> bool {
415 is_http_client(command)
416 || is_file_viewer(command)
417 || is_data_format_tool(command)
418 || is_binary_viewer(command)
419 || is_infra_inspection(command)
420 || is_crypto_command(command)
421 || is_database_query(command)
422 || is_dns_network_inspection(command)
423 || is_language_one_liner(command)
424 || is_container_listing(command)
425 || is_file_listing(command)
426 || is_system_query(command)
427 || is_cloud_cli_query(command)
428 || is_package_manager_info(command)
429 || is_version_or_help(command)
430 || is_config_viewer(command)
431 || is_log_viewer(command)
432 || is_archive_listing(command)
433 || is_clipboard_tool(command)
434 || is_git_data_command(command)
435 || is_task_dry_run(command)
436 || is_env_dump(command)
437}
438
439fn is_verbatim_pipe_tail(command: &str) -> bool {
442 if !command.contains('|') {
443 return false;
444 }
445 let last_segment = command.rsplit('|').next().unwrap_or("").trim();
446 if last_segment.is_empty() {
447 return false;
448 }
449 is_verbatim_single(last_segment)
450}
451
452fn is_http_client(command: &str) -> bool {
453 let first = first_binary(command);
454 matches!(
455 first,
456 "curl" | "wget" | "http" | "https" | "xh" | "curlie" | "grpcurl" | "grpc_cli"
457 )
458}
459
460fn is_file_viewer(command: &str) -> bool {
461 let first = first_binary(command);
462 match first {
463 "cat" | "bat" | "batcat" | "pygmentize" | "highlight" => true,
464 "head" | "tail" => !command.contains("-f") && !command.contains("--follow"),
465 _ => false,
466 }
467}
468
469fn is_data_format_tool(command: &str) -> bool {
470 let first = first_binary(command);
471 matches!(
472 first,
473 "jq" | "yq"
474 | "xq"
475 | "fx"
476 | "gron"
477 | "mlr"
478 | "miller"
479 | "dasel"
480 | "csvlook"
481 | "csvcut"
482 | "csvgrep"
483 | "csvjson"
484 | "in2csv"
485 | "sql2csv"
486 )
487}
488
489fn is_binary_viewer(command: &str) -> bool {
490 let first = first_binary(command);
491 matches!(first, "xxd" | "hexdump" | "od" | "strings" | "file")
492}
493
494fn is_infra_inspection(command: &str) -> bool {
495 let cl = command.trim().to_ascii_lowercase();
496 if cl.starts_with("terraform output")
497 || cl.starts_with("terraform show")
498 || cl.starts_with("terraform state show")
499 || cl.starts_with("terraform state list")
500 || cl.starts_with("terraform state pull")
501 || cl.starts_with("tofu output")
502 || cl.starts_with("tofu show")
503 || cl.starts_with("tofu state show")
504 || cl.starts_with("tofu state list")
505 || cl.starts_with("tofu state pull")
506 || cl.starts_with("pulumi stack output")
507 || cl.starts_with("pulumi stack export")
508 {
509 return true;
510 }
511 if cl.starts_with("docker inspect") || cl.starts_with("podman inspect") {
512 return true;
513 }
514 if (cl.starts_with("kubectl get") || cl.starts_with("k get"))
515 && (cl.contains("-o yaml")
516 || cl.contains("-o json")
517 || cl.contains("-oyaml")
518 || cl.contains("-ojson")
519 || cl.contains("--output yaml")
520 || cl.contains("--output json")
521 || cl.contains("--output=yaml")
522 || cl.contains("--output=json"))
523 {
524 return true;
525 }
526 if cl.starts_with("kubectl describe") || cl.starts_with("k describe") {
527 return true;
528 }
529 if cl.starts_with("helm get") || cl.starts_with("helm template") {
530 return true;
531 }
532 false
533}
534
535fn is_crypto_command(command: &str) -> bool {
536 let first = first_binary(command);
537 if first == "openssl" {
538 return true;
539 }
540 matches!(first, "gpg" | "age" | "ssh-keygen" | "certutil")
541}
542
543fn is_database_query(command: &str) -> bool {
544 let cl = command.to_ascii_lowercase();
545 if cl.starts_with("psql ") && (cl.contains(" -c ") || cl.contains("--command")) {
546 return true;
547 }
548 if cl.starts_with("mysql ") && (cl.contains(" -e ") || cl.contains("--execute")) {
549 return true;
550 }
551 if cl.starts_with("mariadb ") && (cl.contains(" -e ") || cl.contains("--execute")) {
552 return true;
553 }
554 if cl.starts_with("sqlite3 ") && cl.contains('"') {
555 return true;
556 }
557 if cl.starts_with("mongosh ") && cl.contains("--eval") {
558 return true;
559 }
560 false
561}
562
563fn is_dns_network_inspection(command: &str) -> bool {
564 let first = first_binary(command);
565 matches!(
566 first,
567 "dig" | "nslookup" | "host" | "whois" | "drill" | "resolvectl"
568 )
569}
570
571fn is_language_one_liner(command: &str) -> bool {
572 let cl = command.to_ascii_lowercase();
573 (cl.starts_with("python ") || cl.starts_with("python3 "))
574 && (cl.contains(" -c ") || cl.contains(" -c\"") || cl.contains(" -c'"))
575 || (cl.starts_with("node ") && (cl.contains(" -e ") || cl.contains(" --eval")))
576 || (cl.starts_with("ruby ") && cl.contains(" -e "))
577 || (cl.starts_with("perl ") && cl.contains(" -e "))
578 || (cl.starts_with("php ") && cl.contains(" -r "))
579}
580
581fn is_container_listing(command: &str) -> bool {
582 let cl = command.trim().to_ascii_lowercase();
583 if cl.starts_with("docker ps") || cl.starts_with("docker images") {
584 return true;
585 }
586 if cl.starts_with("podman ps") || cl.starts_with("podman images") {
587 return true;
588 }
589 if cl.starts_with("kubectl get") || cl.starts_with("k get") {
590 return true;
591 }
592 if cl.starts_with("helm list") || cl.starts_with("helm ls") {
593 return true;
594 }
595 if cl.starts_with("docker compose ps") || cl.starts_with("docker-compose ps") {
596 return true;
597 }
598 false
599}
600
601fn is_file_listing(command: &str) -> bool {
602 let first = first_binary(command);
603 matches!(
604 first,
605 "find" | "fd" | "fdfind" | "ls" | "exa" | "eza" | "lsd"
606 )
607}
608
609fn is_system_query(command: &str) -> bool {
610 let first = first_binary(command);
611 matches!(
612 first,
613 "stat"
614 | "wc"
615 | "du"
616 | "df"
617 | "free"
618 | "uname"
619 | "id"
620 | "whoami"
621 | "hostname"
622 | "uptime"
623 | "lscpu"
624 | "lsblk"
625 | "ip"
626 | "ifconfig"
627 | "route"
628 | "ss"
629 | "netstat"
630 | "base64"
631 | "sha256sum"
632 | "sha1sum"
633 | "md5sum"
634 | "cksum"
635 | "readlink"
636 | "realpath"
637 | "which"
638 | "type"
639 | "command"
640 )
641}
642
643fn is_cloud_cli_query(command: &str) -> bool {
644 let cl = command.trim().to_ascii_lowercase();
645 let cloud_query_verbs = [
646 "describe",
647 "get",
648 "list",
649 "show",
650 "export",
651 "inspect",
652 "info",
653 "status",
654 "whoami",
655 "caller-identity",
656 "account",
657 ];
658
659 let is_aws = cl.starts_with("aws ") && !cl.starts_with("aws configure");
660 let is_gcloud =
661 cl.starts_with("gcloud ") && !cl.starts_with("gcloud auth") && !cl.contains(" deploy");
662 let is_az = cl.starts_with("az ") && !cl.starts_with("az login");
663
664 if !(is_aws || is_gcloud || is_az) {
665 return false;
666 }
667
668 cloud_query_verbs
669 .iter()
670 .any(|verb| cl.contains(&format!(" {verb}")))
671}
672
673fn is_package_manager_info(command: &str) -> bool {
674 let cl = command.trim().to_ascii_lowercase();
675
676 if cl.starts_with("npm ") {
677 return cl.starts_with("npm list")
678 || cl.starts_with("npm ls")
679 || cl.starts_with("npm info")
680 || cl.starts_with("npm view")
681 || cl.starts_with("npm show")
682 || cl.starts_with("npm outdated")
683 || cl.starts_with("npm audit");
684 }
685 if cl.starts_with("yarn ") {
686 return cl.starts_with("yarn list")
687 || cl.starts_with("yarn info")
688 || cl.starts_with("yarn why")
689 || cl.starts_with("yarn outdated")
690 || cl.starts_with("yarn audit");
691 }
692 if cl.starts_with("pnpm ") {
693 return cl.starts_with("pnpm list")
694 || cl.starts_with("pnpm ls")
695 || cl.starts_with("pnpm why")
696 || cl.starts_with("pnpm outdated")
697 || cl.starts_with("pnpm audit");
698 }
699 if cl.starts_with("pip ") || cl.starts_with("pip3 ") {
700 return cl.contains(" list") || cl.contains(" show") || cl.contains(" freeze");
701 }
702 if cl.starts_with("gem ") {
703 return cl.starts_with("gem list")
704 || cl.starts_with("gem info")
705 || cl.starts_with("gem specification");
706 }
707 if cl.starts_with("cargo ") {
708 return cl.starts_with("cargo metadata")
709 || cl.starts_with("cargo tree")
710 || cl.starts_with("cargo pkgid");
711 }
712 if cl.starts_with("go ") {
713 return cl.starts_with("go list") || cl.starts_with("go version");
714 }
715 if cl.starts_with("composer ") {
716 return cl.starts_with("composer show")
717 || cl.starts_with("composer info")
718 || cl.starts_with("composer outdated");
719 }
720 if cl.starts_with("brew ") {
721 return cl.starts_with("brew list")
722 || cl.starts_with("brew info")
723 || cl.starts_with("brew deps")
724 || cl.starts_with("brew outdated");
725 }
726 if cl.starts_with("apt ") || cl.starts_with("dpkg ") {
727 return cl.starts_with("apt list")
728 || cl.starts_with("apt show")
729 || cl.starts_with("dpkg -l")
730 || cl.starts_with("dpkg --list")
731 || cl.starts_with("dpkg -s");
732 }
733 false
734}
735
736fn is_version_or_help(command: &str) -> bool {
737 let parts: Vec<&str> = command.split_whitespace().collect();
738 if parts.len() < 2 || parts.len() > 3 {
739 return false;
740 }
741 parts.iter().any(|p| {
742 *p == "--version"
743 || *p == "-V"
744 || p.eq_ignore_ascii_case("version")
745 || *p == "--help"
746 || *p == "-h"
747 || p.eq_ignore_ascii_case("help")
748 })
749}
750
751fn is_config_viewer(command: &str) -> bool {
752 let cl = command.trim().to_ascii_lowercase();
753 if cl.starts_with("git config") && !cl.contains("--set") && !cl.contains("--unset") {
754 return true;
755 }
756 if cl.starts_with("npm config list") || cl.starts_with("npm config get") {
757 return true;
758 }
759 if cl.starts_with("yarn config") && !cl.contains(" set") {
760 return true;
761 }
762 if cl.starts_with("pip config list") || cl.starts_with("pip3 config list") {
763 return true;
764 }
765 if cl.starts_with("rustup show") || cl.starts_with("rustup target list") {
766 return true;
767 }
768 if cl.starts_with("docker context ls") || cl.starts_with("docker context list") {
769 return true;
770 }
771 if cl.starts_with("kubectl config")
772 && (cl.contains("view") || cl.contains("get-contexts") || cl.contains("current-context"))
773 {
774 return true;
775 }
776 false
777}
778
779fn is_log_viewer(command: &str) -> bool {
780 let cl = command.trim().to_ascii_lowercase();
781 if cl.starts_with("journalctl") && !cl.contains("-f") && !cl.contains("--follow") {
782 return true;
783 }
784 if cl.starts_with("dmesg") && !cl.contains("-w") && !cl.contains("--follow") {
785 return true;
786 }
787 if cl.starts_with("docker logs") && !cl.contains("-f") && !cl.contains("--follow") {
788 return true;
789 }
790 if cl.starts_with("kubectl logs") && !cl.contains("-f") && !cl.contains("--follow") {
791 return true;
792 }
793 if cl.starts_with("docker compose logs") && !cl.contains("-f") && !cl.contains("--follow") {
794 return true;
795 }
796 false
797}
798
799fn is_archive_listing(command: &str) -> bool {
800 let cl = command.trim().to_ascii_lowercase();
801 if cl.starts_with("tar ") && (cl.contains(" -tf") || cl.contains(" -t") || cl.contains(" tf")) {
802 return true;
803 }
804 if cl.starts_with("unzip -l") || cl.starts_with("unzip -Z") {
805 return true;
806 }
807 let first = first_binary(command);
808 matches!(first, "zipinfo" | "lsar" | "7z" if cl.contains(" l ") || cl.contains(" l\t"))
809 || first == "zipinfo"
810 || first == "lsar"
811}
812
813fn is_clipboard_tool(command: &str) -> bool {
814 let first = first_binary(command);
815 if matches!(first, "pbpaste" | "wl-paste") {
816 return true;
817 }
818 let cl = command.trim().to_ascii_lowercase();
819 if cl.starts_with("xclip") && cl.contains("-o") {
820 return true;
821 }
822 if cl.starts_with("xsel") && (cl.contains("-o") || cl.contains("--output")) {
823 return true;
824 }
825 false
826}
827
828fn is_git_data_command(command: &str) -> bool {
829 let cl = command.trim().to_ascii_lowercase();
830 if !cl.contains("git") {
831 return false;
832 }
833 let exact_data_subs = [
834 "remote",
835 "rev-parse",
836 "rev-list",
837 "ls-files",
838 "ls-tree",
839 "ls-remote",
840 "shortlog",
841 "for-each-ref",
842 "cat-file",
843 "name-rev",
844 "describe",
845 "merge-base",
846 ];
847
848 let mut tokens = cl.split_whitespace();
849 while let Some(tok) = tokens.next() {
850 let base = tok.rsplit('/').next().unwrap_or(tok);
851 if base != "git" {
852 continue;
853 }
854 let mut skip_next = false;
855 for arg in tokens.by_ref() {
856 if skip_next {
857 skip_next = false;
858 continue;
859 }
860 if arg == "-c" || arg == "-C" || arg == "--git-dir" || arg == "--work-tree" {
861 skip_next = true;
862 continue;
863 }
864 if arg.starts_with('-') {
865 continue;
866 }
867 return exact_data_subs.contains(&arg);
868 }
869 return false;
870 }
871 false
872}
873
874fn is_task_dry_run(command: &str) -> bool {
875 let cl = command.trim().to_ascii_lowercase();
876 if cl.starts_with("make ") && (cl.contains(" -n") || cl.contains(" --dry-run")) {
877 return true;
878 }
879 if cl.starts_with("ansible") && (cl.contains("--check") || cl.contains("--diff")) {
880 return true;
881 }
882 false
883}
884
885fn is_env_dump(command: &str) -> bool {
886 let first = first_binary(command);
887 matches!(first, "env" | "printenv" | "set" | "export" | "locale")
888}
889
890fn first_binary(command: &str) -> &str {
892 let first = command.split_whitespace().next().unwrap_or("");
893 first.rsplit('/').next().unwrap_or(first)
894}
895
896fn is_standalone_diff_command(command: &str) -> bool {
898 let first = command.split_whitespace().next().unwrap_or("");
899 let base = first.rsplit('/').next().unwrap_or(first);
900 base.eq_ignore_ascii_case("diff")
901 || base.eq_ignore_ascii_case("colordiff")
902 || base.eq_ignore_ascii_case("icdiff")
903 || base.eq_ignore_ascii_case("delta")
904}
905
906fn is_structural_git_command(command: &str) -> bool {
908 let mut tokens = command.split_whitespace();
909 while let Some(tok) = tokens.next() {
910 let base = tok.rsplit('/').next().unwrap_or(tok);
911 if !base.eq_ignore_ascii_case("git") {
912 continue;
913 }
914 let mut skip_next = false;
915 let remaining: Vec<&str> = tokens.collect();
916 for arg in &remaining {
917 if skip_next {
918 skip_next = false;
919 continue;
920 }
921 if *arg == "-C" || *arg == "-c" || *arg == "--git-dir" || *arg == "--work-tree" {
922 skip_next = true;
923 continue;
924 }
925 if arg.starts_with('-') {
926 continue;
927 }
928 let sub = arg.to_ascii_lowercase();
929 return match sub.as_str() {
930 "diff" | "show" | "blame" => true,
931 "log" => has_patch_flag(&remaining),
932 "stash" => remaining.iter().any(|a| a.eq_ignore_ascii_case("show")),
933 _ => false,
934 };
935 }
936 return false;
937 }
938 false
939}
940
941fn has_patch_flag(args: &[&str]) -> bool {
943 args.iter()
944 .any(|a| *a == "-p" || *a == "--patch" || a.starts_with("-p"))
945}
946
947fn compress_if_beneficial(command: &str, output: &str) -> String {
948 if output.trim().is_empty() {
949 return String::new();
950 }
951
952 if !is_search_output(command) && crate::tools::ctx_shell::contains_auth_flow(output) {
953 return output.to_string();
954 }
955
956 let original_tokens = count_tokens(output);
957
958 if original_tokens < 50 {
959 return output.to_string();
960 }
961
962 let min_output_tokens = 5;
963
964 if is_verbatim_output(command) {
965 return truncate_verbatim(output, original_tokens);
966 }
967
968 if has_structural_output(command) {
969 let cl = command.to_ascii_lowercase();
970 if let Some(compressed) = patterns::try_specific_pattern(&cl, output) {
971 if !compressed.trim().is_empty() {
972 let compressed_tokens = count_tokens(&compressed);
973 if compressed_tokens >= min_output_tokens && compressed_tokens < original_tokens {
974 let saved = original_tokens - compressed_tokens;
975 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
976 if pct >= 5 {
977 return format!(
978 "{compressed}\n[lean-ctx: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
979 );
980 }
981 return compressed;
982 }
983 }
984 }
985 return output.to_string();
986 }
987
988 if let Some(mut compressed) = patterns::compress_output(command, output) {
989 if !compressed.trim().is_empty() {
990 let config = crate::core::config::Config::load();
991 let level = crate::core::config::CompressionLevel::effective(&config);
992 if level.is_active() {
993 let terse_result =
994 crate::core::terse::pipeline::compress(output, &level, Some(&compressed));
995 if terse_result.quality_passed {
996 compressed = terse_result.output;
997 }
998 }
999
1000 let compressed_tokens = count_tokens(&compressed);
1001 if compressed_tokens >= min_output_tokens && compressed_tokens < original_tokens {
1002 let ratio = compressed_tokens as f64 / original_tokens as f64;
1003 if ratio < 0.05 && original_tokens > 100 && original_tokens < 2000 {
1004 tracing::warn!("compression removed >95% of small output, returning original");
1005 return output.to_string();
1006 }
1007 let saved = original_tokens - compressed_tokens;
1008 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1009 if pct >= 5 {
1010 return format!(
1011 "{compressed}\n[lean-ctx: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
1012 );
1013 }
1014 return compressed;
1015 }
1016 if compressed_tokens < min_output_tokens {
1017 return output.to_string();
1018 }
1019 }
1020 }
1021
1022 {
1023 let config = crate::core::config::Config::load();
1024 let level = crate::core::config::CompressionLevel::effective(&config);
1025 if level.is_active() {
1026 let terse_result = crate::core::terse::pipeline::compress(output, &level, None);
1027 if terse_result.quality_passed && terse_result.savings_pct >= 3.0 {
1028 let tok_before = terse_result.tokens_before;
1029 let tok_after = terse_result.tokens_after;
1030 let pct = terse_result.savings_pct.round() as usize;
1031 return format!(
1032 "{}\n[lean-ctx: {tok_before}→{tok_after} tok, -{pct}%]",
1033 terse_result.output
1034 );
1035 }
1036 }
1037 }
1038
1039 let cleaned = crate::core::compressor::lightweight_cleanup(output);
1040 let cleaned_tokens = count_tokens(&cleaned);
1041 if cleaned_tokens < original_tokens {
1042 let lines: Vec<&str> = cleaned.lines().collect();
1043 if lines.len() > 30 {
1044 let compressed = truncate_with_safety_scan(&lines, original_tokens);
1045 if let Some(c) = compressed {
1046 return c;
1047 }
1048 }
1049 if cleaned_tokens < original_tokens {
1050 let saved = original_tokens - cleaned_tokens;
1051 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1052 if pct >= 5 {
1053 return format!(
1054 "{cleaned}\n[lean-ctx: {original_tokens}→{cleaned_tokens} tok, -{pct}%]"
1055 );
1056 }
1057 return cleaned;
1058 }
1059 }
1060
1061 let lines: Vec<&str> = output.lines().collect();
1062 if lines.len() > 30 {
1063 if let Some(c) = truncate_with_safety_scan(&lines, original_tokens) {
1064 return c;
1065 }
1066 }
1067
1068 output.to_string()
1069}
1070
1071const MAX_VERBATIM_TOKENS: usize = 8000;
1072
1073fn truncate_verbatim(output: &str, original_tokens: usize) -> String {
1075 if original_tokens <= MAX_VERBATIM_TOKENS {
1076 return output.to_string();
1077 }
1078 let lines: Vec<&str> = output.lines().collect();
1079 let total = lines.len();
1080 if total <= 60 {
1081 return output.to_string();
1082 }
1083 let head = 30.min(total);
1084 let tail = 20.min(total.saturating_sub(head));
1085 let omitted = total - head - tail;
1086 let mut result = String::with_capacity(output.len() / 2);
1087 for line in &lines[..head] {
1088 result.push_str(line);
1089 result.push('\n');
1090 }
1091 result.push_str(&format!(
1092 "\n[{omitted} lines omitted — output too large for context window]\n\n"
1093 ));
1094 for line in lines.iter().skip(total - tail) {
1095 result.push_str(line);
1096 result.push('\n');
1097 }
1098 let truncated_tokens = count_tokens(&result);
1099 result.push_str(&format!(
1100 "[lean-ctx: {original_tokens}→{truncated_tokens} tok, verbatim truncated]"
1101 ));
1102 result
1103}
1104
1105fn truncate_with_safety_scan(lines: &[&str], original_tokens: usize) -> Option<String> {
1106 use crate::core::safety_needles;
1107
1108 let first = &lines[..5];
1109 let last = &lines[lines.len() - 5..];
1110 let middle = &lines[5..lines.len() - 5];
1111
1112 let safety_lines = safety_needles::extract_safety_lines(middle, 20);
1113 let safety_count = safety_lines.len();
1114 let omitted = middle.len() - safety_count;
1115
1116 let mut parts = Vec::new();
1117 parts.push(first.join("\n"));
1118 if safety_count > 0 {
1119 parts.push(format!(
1120 "[{omitted} lines omitted, {safety_count} safety-relevant lines preserved]"
1121 ));
1122 parts.push(safety_lines.join("\n"));
1123 } else {
1124 parts.push(format!("[{omitted} lines omitted]"));
1125 }
1126 parts.push(last.join("\n"));
1127
1128 let compressed = parts.join("\n");
1129 let ct = count_tokens(&compressed);
1130 if ct >= original_tokens {
1131 return None;
1132 }
1133 let saved = original_tokens - ct;
1134 let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1135 if pct >= 5 {
1136 Some(format!(
1137 "{compressed}\n[lean-ctx: {original_tokens}→{ct} tok, -{pct}%]"
1138 ))
1139 } else {
1140 Some(compressed)
1141 }
1142}
1143
1144pub fn compress_if_beneficial_pub(command: &str, output: &str) -> String {
1146 compress_if_beneficial(command, output)
1147}
1148
1149#[cfg(test)]
1150mod passthrough_tests {
1151 use super::is_excluded_command;
1152
1153 #[test]
1154 fn turbo_is_passthrough() {
1155 assert!(is_excluded_command("turbo run dev", &[]));
1156 assert!(is_excluded_command("turbo run build", &[]));
1157 assert!(is_excluded_command("pnpm turbo run dev", &[]));
1158 assert!(is_excluded_command("npx turbo run dev", &[]));
1159 }
1160
1161 #[test]
1162 fn dev_servers_are_passthrough() {
1163 assert!(is_excluded_command("next dev", &[]));
1164 assert!(is_excluded_command("vite dev", &[]));
1165 assert!(is_excluded_command("nuxt dev", &[]));
1166 assert!(is_excluded_command("astro dev", &[]));
1167 assert!(is_excluded_command("nodemon server.js", &[]));
1168 }
1169
1170 #[test]
1171 fn interactive_tools_are_passthrough() {
1172 assert!(is_excluded_command("vim file.rs", &[]));
1173 assert!(is_excluded_command("nvim", &[]));
1174 assert!(is_excluded_command("htop", &[]));
1175 assert!(is_excluded_command("ssh user@host", &[]));
1176 assert!(is_excluded_command("tail -f /var/log/syslog", &[]));
1177 }
1178
1179 #[test]
1180 fn docker_streaming_is_passthrough() {
1181 assert!(is_excluded_command("docker logs my-container", &[]));
1182 assert!(is_excluded_command("docker logs -f webapp", &[]));
1183 assert!(is_excluded_command("docker attach my-container", &[]));
1184 assert!(is_excluded_command("docker exec -it web bash", &[]));
1185 assert!(is_excluded_command("docker exec -ti web bash", &[]));
1186 assert!(is_excluded_command("docker run -it ubuntu bash", &[]));
1187 assert!(is_excluded_command("docker compose exec web bash", &[]));
1188 assert!(is_excluded_command("docker stats", &[]));
1189 assert!(is_excluded_command("docker events", &[]));
1190 }
1191
1192 #[test]
1193 fn kubectl_is_passthrough() {
1194 assert!(is_excluded_command("kubectl logs my-pod", &[]));
1195 assert!(is_excluded_command("kubectl logs -f deploy/web", &[]));
1196 assert!(is_excluded_command("kubectl exec -it pod -- bash", &[]));
1197 assert!(is_excluded_command(
1198 "kubectl port-forward svc/web 8080:80",
1199 &[]
1200 ));
1201 assert!(is_excluded_command("kubectl attach my-pod", &[]));
1202 assert!(is_excluded_command("kubectl proxy", &[]));
1203 }
1204
1205 #[test]
1206 fn database_repls_are_passthrough() {
1207 assert!(is_excluded_command("psql -U user mydb", &[]));
1208 assert!(is_excluded_command("mysql -u root -p", &[]));
1209 assert!(is_excluded_command("sqlite3 data.db", &[]));
1210 assert!(is_excluded_command("redis-cli", &[]));
1211 assert!(is_excluded_command("mongosh", &[]));
1212 }
1213
1214 #[test]
1215 fn streaming_tools_are_passthrough() {
1216 assert!(is_excluded_command("journalctl -f", &[]));
1217 assert!(is_excluded_command("ping 8.8.8.8", &[]));
1218 assert!(is_excluded_command("strace -p 1234", &[]));
1219 assert!(is_excluded_command("tcpdump -i eth0", &[]));
1220 assert!(is_excluded_command("tail -F /var/log/app.log", &[]));
1221 assert!(is_excluded_command("tmux new -s work", &[]));
1222 assert!(is_excluded_command("screen -S dev", &[]));
1223 }
1224
1225 #[test]
1226 fn additional_dev_servers_are_passthrough() {
1227 assert!(is_excluded_command("gatsby develop", &[]));
1228 assert!(is_excluded_command("ng serve --port 4200", &[]));
1229 assert!(is_excluded_command("remix dev", &[]));
1230 assert!(is_excluded_command("wrangler dev", &[]));
1231 assert!(is_excluded_command("hugo server", &[]));
1232 assert!(is_excluded_command("bun dev", &[]));
1233 assert!(is_excluded_command("cargo watch -x test", &[]));
1234 }
1235
1236 #[test]
1237 fn normal_commands_not_excluded() {
1238 assert!(!is_excluded_command("git status", &[]));
1239 assert!(!is_excluded_command("cargo test", &[]));
1240 assert!(!is_excluded_command("npm run build", &[]));
1241 assert!(!is_excluded_command("ls -la", &[]));
1242 }
1243
1244 #[test]
1245 fn user_exclusions_work() {
1246 let excl = vec!["myapp".to_string()];
1247 assert!(is_excluded_command("myapp serve", &excl));
1248 assert!(!is_excluded_command("git status", &excl));
1249 }
1250
1251 #[test]
1252 fn auth_commands_excluded() {
1253 assert!(is_excluded_command("az login --use-device-code", &[]));
1254 assert!(is_excluded_command("gh auth login", &[]));
1255 assert!(is_excluded_command("gh pr close --comment 'done'", &[]));
1256 assert!(is_excluded_command("gh issue list", &[]));
1257 assert!(is_excluded_command("gcloud auth login", &[]));
1258 assert!(is_excluded_command("aws sso login", &[]));
1259 assert!(is_excluded_command("firebase login", &[]));
1260 assert!(is_excluded_command("vercel login", &[]));
1261 assert!(is_excluded_command("heroku login", &[]));
1262 assert!(is_excluded_command("az login", &[]));
1263 assert!(is_excluded_command("kubelogin convert-kubeconfig", &[]));
1264 assert!(is_excluded_command("vault login -method=oidc", &[]));
1265 assert!(is_excluded_command("flyctl auth login", &[]));
1266 }
1267
1268 #[test]
1269 fn auth_exclusion_does_not_affect_normal_commands() {
1270 assert!(!is_excluded_command("git log", &[]));
1271 assert!(!is_excluded_command("npm run build", &[]));
1272 assert!(!is_excluded_command("cargo test", &[]));
1273 assert!(!is_excluded_command("aws s3 ls", &[]));
1274 assert!(!is_excluded_command("gcloud compute instances list", &[]));
1275 assert!(!is_excluded_command("az vm list", &[]));
1276 }
1277
1278 #[test]
1279 fn npm_script_runners_are_passthrough() {
1280 assert!(is_excluded_command("npm run dev", &[]));
1281 assert!(is_excluded_command("npm run start", &[]));
1282 assert!(is_excluded_command("npm run serve", &[]));
1283 assert!(is_excluded_command("npm run watch", &[]));
1284 assert!(is_excluded_command("npm run preview", &[]));
1285 assert!(is_excluded_command("npm run storybook", &[]));
1286 assert!(is_excluded_command("npm run test:watch", &[]));
1287 assert!(is_excluded_command("npm start", &[]));
1288 assert!(is_excluded_command("npx vite", &[]));
1289 assert!(is_excluded_command("npx next dev", &[]));
1290 }
1291
1292 #[test]
1293 fn pnpm_script_runners_are_passthrough() {
1294 assert!(is_excluded_command("pnpm run dev", &[]));
1295 assert!(is_excluded_command("pnpm run start", &[]));
1296 assert!(is_excluded_command("pnpm run serve", &[]));
1297 assert!(is_excluded_command("pnpm run watch", &[]));
1298 assert!(is_excluded_command("pnpm run preview", &[]));
1299 assert!(is_excluded_command("pnpm dev", &[]));
1300 assert!(is_excluded_command("pnpm start", &[]));
1301 assert!(is_excluded_command("pnpm preview", &[]));
1302 }
1303
1304 #[test]
1305 fn yarn_script_runners_are_passthrough() {
1306 assert!(is_excluded_command("yarn dev", &[]));
1307 assert!(is_excluded_command("yarn start", &[]));
1308 assert!(is_excluded_command("yarn serve", &[]));
1309 assert!(is_excluded_command("yarn watch", &[]));
1310 assert!(is_excluded_command("yarn preview", &[]));
1311 assert!(is_excluded_command("yarn storybook", &[]));
1312 }
1313
1314 #[test]
1315 fn bun_deno_script_runners_are_passthrough() {
1316 assert!(is_excluded_command("bun run dev", &[]));
1317 assert!(is_excluded_command("bun run start", &[]));
1318 assert!(is_excluded_command("bun run serve", &[]));
1319 assert!(is_excluded_command("bun run watch", &[]));
1320 assert!(is_excluded_command("bun run preview", &[]));
1321 assert!(is_excluded_command("bun start", &[]));
1322 assert!(is_excluded_command("deno task dev", &[]));
1323 assert!(is_excluded_command("deno task start", &[]));
1324 assert!(is_excluded_command("deno task serve", &[]));
1325 assert!(is_excluded_command("deno run --watch main.ts", &[]));
1326 }
1327
1328 #[test]
1329 fn python_servers_are_passthrough() {
1330 assert!(is_excluded_command("flask run --port 5000", &[]));
1331 assert!(is_excluded_command("uvicorn app:app --reload", &[]));
1332 assert!(is_excluded_command("gunicorn app:app -w 4", &[]));
1333 assert!(is_excluded_command("hypercorn app:app", &[]));
1334 assert!(is_excluded_command("daphne app.asgi:application", &[]));
1335 assert!(is_excluded_command(
1336 "django-admin runserver 0.0.0.0:8000",
1337 &[]
1338 ));
1339 assert!(is_excluded_command("python manage.py runserver", &[]));
1340 assert!(is_excluded_command("python -m http.server 8080", &[]));
1341 assert!(is_excluded_command("python3 -m http.server", &[]));
1342 assert!(is_excluded_command("streamlit run app.py", &[]));
1343 assert!(is_excluded_command("gradio app.py", &[]));
1344 assert!(is_excluded_command("celery worker -A app", &[]));
1345 assert!(is_excluded_command("celery -A app worker", &[]));
1346 assert!(is_excluded_command("celery -B", &[]));
1347 assert!(is_excluded_command("dramatiq tasks", &[]));
1348 assert!(is_excluded_command("rq worker", &[]));
1349 assert!(is_excluded_command("ptw tests/", &[]));
1350 assert!(is_excluded_command("pytest-watch", &[]));
1351 }
1352
1353 #[test]
1354 fn ruby_servers_are_passthrough() {
1355 assert!(is_excluded_command("rails server -p 3000", &[]));
1356 assert!(is_excluded_command("rails s", &[]));
1357 assert!(is_excluded_command("puma -C config.rb", &[]));
1358 assert!(is_excluded_command("unicorn -c config.rb", &[]));
1359 assert!(is_excluded_command("thin start", &[]));
1360 assert!(is_excluded_command("foreman start", &[]));
1361 assert!(is_excluded_command("overmind start", &[]));
1362 assert!(is_excluded_command("guard -G Guardfile", &[]));
1363 assert!(is_excluded_command("sidekiq", &[]));
1364 assert!(is_excluded_command("resque work", &[]));
1365 }
1366
1367 #[test]
1368 fn php_servers_are_passthrough() {
1369 assert!(is_excluded_command("php artisan serve", &[]));
1370 assert!(is_excluded_command("php -S localhost:8000", &[]));
1371 assert!(is_excluded_command("php artisan queue:work", &[]));
1372 assert!(is_excluded_command("php artisan queue:listen", &[]));
1373 assert!(is_excluded_command("php artisan horizon", &[]));
1374 assert!(is_excluded_command("php artisan tinker", &[]));
1375 assert!(is_excluded_command("sail up", &[]));
1376 }
1377
1378 #[test]
1379 fn java_servers_are_passthrough() {
1380 assert!(is_excluded_command("./gradlew bootRun", &[]));
1381 assert!(is_excluded_command("gradlew bootRun", &[]));
1382 assert!(is_excluded_command("gradle bootRun", &[]));
1383 assert!(is_excluded_command("mvn spring-boot:run", &[]));
1384 assert!(is_excluded_command("./mvnw spring-boot:run", &[]));
1385 assert!(is_excluded_command("mvn quarkus:dev", &[]));
1386 assert!(is_excluded_command("./mvnw quarkus:dev", &[]));
1387 assert!(is_excluded_command("sbt run", &[]));
1388 assert!(is_excluded_command("sbt ~compile", &[]));
1389 assert!(is_excluded_command("lein run", &[]));
1390 assert!(is_excluded_command("lein repl", &[]));
1391 assert!(is_excluded_command("./gradlew run", &[]));
1392 }
1393
1394 #[test]
1395 fn go_servers_are_passthrough() {
1396 assert!(is_excluded_command("go run main.go", &[]));
1397 assert!(is_excluded_command("go run ./cmd/server", &[]));
1398 assert!(is_excluded_command("air -c .air.toml", &[]));
1399 assert!(is_excluded_command("gin --port 3000", &[]));
1400 assert!(is_excluded_command("realize start", &[]));
1401 assert!(is_excluded_command("reflex -r '.go$' go run .", &[]));
1402 assert!(is_excluded_command("gowatch run", &[]));
1403 }
1404
1405 #[test]
1406 fn dotnet_servers_are_passthrough() {
1407 assert!(is_excluded_command("dotnet run", &[]));
1408 assert!(is_excluded_command("dotnet run --project src/Api", &[]));
1409 assert!(is_excluded_command("dotnet watch run", &[]));
1410 assert!(is_excluded_command("dotnet ef database update", &[]));
1411 }
1412
1413 #[test]
1414 fn elixir_servers_are_passthrough() {
1415 assert!(is_excluded_command("mix phx.server", &[]));
1416 assert!(is_excluded_command("iex -s mix phx.server", &[]));
1417 assert!(is_excluded_command("iex -S mix phx.server", &[]));
1418 }
1419
1420 #[test]
1421 fn swift_zig_servers_are_passthrough() {
1422 assert!(is_excluded_command("swift run MyApp", &[]));
1423 assert!(is_excluded_command("swift package resolve", &[]));
1424 assert!(is_excluded_command("vapor serve --port 8080", &[]));
1425 assert!(is_excluded_command("zig build run", &[]));
1426 }
1427
1428 #[test]
1429 fn rust_watchers_are_passthrough() {
1430 assert!(is_excluded_command("cargo watch -x test", &[]));
1431 assert!(is_excluded_command("cargo run --bin server", &[]));
1432 assert!(is_excluded_command("cargo leptos watch", &[]));
1433 assert!(is_excluded_command("bacon test", &[]));
1434 }
1435
1436 #[test]
1437 fn general_task_runners_are_passthrough() {
1438 assert!(is_excluded_command("make dev", &[]));
1439 assert!(is_excluded_command("make serve", &[]));
1440 assert!(is_excluded_command("make watch", &[]));
1441 assert!(is_excluded_command("make run", &[]));
1442 assert!(is_excluded_command("make start", &[]));
1443 assert!(is_excluded_command("just dev", &[]));
1444 assert!(is_excluded_command("just serve", &[]));
1445 assert!(is_excluded_command("just watch", &[]));
1446 assert!(is_excluded_command("just start", &[]));
1447 assert!(is_excluded_command("just run", &[]));
1448 assert!(is_excluded_command("task dev", &[]));
1449 assert!(is_excluded_command("task serve", &[]));
1450 assert!(is_excluded_command("task watch", &[]));
1451 assert!(is_excluded_command("nix develop", &[]));
1452 assert!(is_excluded_command("devenv up", &[]));
1453 }
1454
1455 #[test]
1456 fn cicd_infra_are_passthrough() {
1457 assert!(is_excluded_command("act push", &[]));
1458 assert!(is_excluded_command("docker compose watch", &[]));
1459 assert!(is_excluded_command("docker-compose watch", &[]));
1460 assert!(is_excluded_command("skaffold dev", &[]));
1461 assert!(is_excluded_command("tilt up", &[]));
1462 assert!(is_excluded_command("garden dev", &[]));
1463 assert!(is_excluded_command("telepresence connect", &[]));
1464 }
1465
1466 #[test]
1467 fn networking_monitoring_are_passthrough() {
1468 assert!(is_excluded_command("mtr 8.8.8.8", &[]));
1469 assert!(is_excluded_command("nmap -sV host", &[]));
1470 assert!(is_excluded_command("iperf -s", &[]));
1471 assert!(is_excluded_command("iperf3 -c host", &[]));
1472 assert!(is_excluded_command("socat TCP-LISTEN:8080,fork -", &[]));
1473 }
1474
1475 #[test]
1476 fn load_testing_is_passthrough() {
1477 assert!(is_excluded_command("ab -n 1000 http://localhost/", &[]));
1478 assert!(is_excluded_command("wrk -t12 -c400 http://localhost/", &[]));
1479 assert!(is_excluded_command("hey -n 10000 http://localhost/", &[]));
1480 assert!(is_excluded_command("vegeta attack", &[]));
1481 assert!(is_excluded_command("k6 run script.js", &[]));
1482 assert!(is_excluded_command("artillery run test.yml", &[]));
1483 }
1484
1485 #[test]
1486 fn smart_script_detection_works() {
1487 assert!(is_excluded_command("npm run dev:ssr", &[]));
1488 assert!(is_excluded_command("npm run dev:local", &[]));
1489 assert!(is_excluded_command("yarn start:production", &[]));
1490 assert!(is_excluded_command("pnpm run serve:local", &[]));
1491 assert!(is_excluded_command("bun run watch:css", &[]));
1492 assert!(is_excluded_command("deno task dev:api", &[]));
1493 assert!(is_excluded_command("npm run storybook:ci", &[]));
1494 assert!(is_excluded_command("yarn preview:staging", &[]));
1495 assert!(is_excluded_command("pnpm run hot-reload", &[]));
1496 assert!(is_excluded_command("npm run hmr-server", &[]));
1497 assert!(is_excluded_command("bun run live-server", &[]));
1498 }
1499
1500 #[test]
1501 fn smart_detection_does_not_false_positive() {
1502 assert!(!is_excluded_command("npm run build", &[]));
1503 assert!(!is_excluded_command("npm run lint", &[]));
1504 assert!(!is_excluded_command("npm run test", &[]));
1505 assert!(!is_excluded_command("npm run format", &[]));
1506 assert!(!is_excluded_command("yarn build", &[]));
1507 assert!(!is_excluded_command("yarn test", &[]));
1508 assert!(!is_excluded_command("pnpm run lint", &[]));
1509 assert!(!is_excluded_command("bun run build", &[]));
1510 }
1511
1512 #[test]
1513 fn gh_fully_excluded() {
1514 assert!(is_excluded_command("gh", &[]));
1515 assert!(is_excluded_command(
1516 "gh pr close --comment 'closing — see #407'",
1517 &[]
1518 ));
1519 assert!(is_excluded_command(
1520 "gh issue create --title \"bug\" --body \"desc\"",
1521 &[]
1522 ));
1523 assert!(is_excluded_command("gh api repos/owner/repo/pulls", &[]));
1524 assert!(is_excluded_command("gh run list --limit 5", &[]));
1525 }
1526}
1527
1528#[cfg(test)]
1529mod verbatim_output_tests {
1530 use super::{compress_if_beneficial, is_verbatim_output};
1531
1532 #[test]
1533 fn http_clients_are_verbatim() {
1534 assert!(is_verbatim_output("curl https://api.example.com"));
1535 assert!(is_verbatim_output(
1536 "curl -s -H 'Accept: application/json' https://api.example.com/data"
1537 ));
1538 assert!(is_verbatim_output(
1539 "curl -X POST -d '{\"key\":\"val\"}' https://api.example.com"
1540 ));
1541 assert!(is_verbatim_output("/usr/bin/curl https://example.com"));
1542 assert!(is_verbatim_output("wget -qO- https://example.com"));
1543 assert!(is_verbatim_output("wget https://example.com/file.json"));
1544 assert!(is_verbatim_output("http GET https://api.example.com"));
1545 assert!(is_verbatim_output("https PUT https://api.example.com/data"));
1546 assert!(is_verbatim_output("xh https://api.example.com"));
1547 assert!(is_verbatim_output("curlie https://api.example.com"));
1548 assert!(is_verbatim_output(
1549 "grpcurl -plaintext localhost:50051 list"
1550 ));
1551 }
1552
1553 #[test]
1554 fn file_viewers_are_verbatim() {
1555 assert!(is_verbatim_output("cat package.json"));
1556 assert!(is_verbatim_output("cat /etc/hosts"));
1557 assert!(is_verbatim_output("/bin/cat file.txt"));
1558 assert!(is_verbatim_output("bat src/main.rs"));
1559 assert!(is_verbatim_output("batcat README.md"));
1560 assert!(is_verbatim_output("head -20 log.txt"));
1561 assert!(is_verbatim_output("head -n 50 file.rs"));
1562 assert!(is_verbatim_output("tail -100 server.log"));
1563 assert!(is_verbatim_output("tail -n 20 file.txt"));
1564 }
1565
1566 #[test]
1567 fn tail_follow_not_verbatim() {
1568 assert!(!is_verbatim_output("tail -f /var/log/syslog"));
1569 assert!(!is_verbatim_output("tail --follow server.log"));
1570 }
1571
1572 #[test]
1573 fn data_format_tools_are_verbatim() {
1574 assert!(is_verbatim_output("jq '.items' data.json"));
1575 assert!(is_verbatim_output("jq -r '.name' package.json"));
1576 assert!(is_verbatim_output("yq '.spec' deployment.yaml"));
1577 assert!(is_verbatim_output("xq '.rss.channel.title' feed.xml"));
1578 assert!(is_verbatim_output("fx data.json"));
1579 assert!(is_verbatim_output("gron data.json"));
1580 assert!(is_verbatim_output("mlr --csv head -n 5 data.csv"));
1581 assert!(is_verbatim_output("miller --json head data.json"));
1582 assert!(is_verbatim_output("dasel -f config.toml '.database.host'"));
1583 assert!(is_verbatim_output("csvlook data.csv"));
1584 assert!(is_verbatim_output("csvcut -c 1,3 data.csv"));
1585 assert!(is_verbatim_output("csvjson data.csv"));
1586 }
1587
1588 #[test]
1589 fn binary_viewers_are_verbatim() {
1590 assert!(is_verbatim_output("xxd binary.dat"));
1591 assert!(is_verbatim_output("hexdump -C binary.dat"));
1592 assert!(is_verbatim_output("od -A x -t x1z binary.dat"));
1593 assert!(is_verbatim_output("strings /usr/bin/curl"));
1594 assert!(is_verbatim_output("file unknown.bin"));
1595 }
1596
1597 #[test]
1598 fn infra_inspection_is_verbatim() {
1599 assert!(is_verbatim_output("terraform output"));
1600 assert!(is_verbatim_output("terraform show"));
1601 assert!(is_verbatim_output("terraform state show aws_instance.web"));
1602 assert!(is_verbatim_output("terraform state list"));
1603 assert!(is_verbatim_output("terraform state pull"));
1604 assert!(is_verbatim_output("tofu output"));
1605 assert!(is_verbatim_output("tofu show"));
1606 assert!(is_verbatim_output("pulumi stack output"));
1607 assert!(is_verbatim_output("pulumi stack export"));
1608 assert!(is_verbatim_output("docker inspect my-container"));
1609 assert!(is_verbatim_output("podman inspect my-pod"));
1610 assert!(is_verbatim_output("kubectl get pods -o yaml"));
1611 assert!(is_verbatim_output("kubectl get deploy -ojson"));
1612 assert!(is_verbatim_output("kubectl get svc --output yaml"));
1613 assert!(is_verbatim_output("kubectl get pods --output=json"));
1614 assert!(is_verbatim_output("k get pods -o yaml"));
1615 assert!(is_verbatim_output("kubectl describe pod my-pod"));
1616 assert!(is_verbatim_output("k describe deployment web"));
1617 assert!(is_verbatim_output("helm get values my-release"));
1618 assert!(is_verbatim_output("helm template my-chart"));
1619 }
1620
1621 #[test]
1622 fn terraform_plan_not_verbatim() {
1623 assert!(!is_verbatim_output("terraform plan"));
1624 assert!(!is_verbatim_output("terraform apply"));
1625 assert!(!is_verbatim_output("terraform init"));
1626 }
1627
1628 #[test]
1629 fn kubectl_get_is_now_verbatim() {
1630 assert!(is_verbatim_output("kubectl get pods"));
1631 assert!(is_verbatim_output("kubectl get deployments"));
1632 }
1633
1634 #[test]
1635 fn crypto_commands_are_verbatim() {
1636 assert!(is_verbatim_output("openssl x509 -in cert.pem -text"));
1637 assert!(is_verbatim_output(
1638 "openssl s_client -connect example.com:443"
1639 ));
1640 assert!(is_verbatim_output("openssl req -new -x509 -key key.pem"));
1641 assert!(is_verbatim_output("gpg --list-keys"));
1642 assert!(is_verbatim_output("ssh-keygen -l -f key.pub"));
1643 }
1644
1645 #[test]
1646 fn database_queries_are_verbatim() {
1647 assert!(is_verbatim_output(r#"psql -c "SELECT * FROM users" mydb"#));
1648 assert!(is_verbatim_output("psql --command 'SELECT 1' mydb"));
1649 assert!(is_verbatim_output(r#"mysql -e "SELECT * FROM users" mydb"#));
1650 assert!(is_verbatim_output("mysql --execute 'SHOW TABLES' mydb"));
1651 assert!(is_verbatim_output(
1652 r#"mariadb -e "SELECT * FROM users" mydb"#
1653 ));
1654 assert!(is_verbatim_output(
1655 r#"sqlite3 data.db "SELECT * FROM users""#
1656 ));
1657 assert!(is_verbatim_output("mongosh --eval 'db.users.find()' mydb"));
1658 }
1659
1660 #[test]
1661 fn interactive_db_not_verbatim() {
1662 assert!(!is_verbatim_output("psql mydb"));
1663 assert!(!is_verbatim_output("mysql -u root mydb"));
1664 }
1665
1666 #[test]
1667 fn dns_network_inspection_is_verbatim() {
1668 assert!(is_verbatim_output("dig example.com"));
1669 assert!(is_verbatim_output("dig +short example.com A"));
1670 assert!(is_verbatim_output("nslookup example.com"));
1671 assert!(is_verbatim_output("host example.com"));
1672 assert!(is_verbatim_output("whois example.com"));
1673 assert!(is_verbatim_output("drill example.com"));
1674 }
1675
1676 #[test]
1677 fn language_one_liners_are_verbatim() {
1678 assert!(is_verbatim_output(
1679 "python -c 'import json; print(json.dumps({\"key\": \"value\"}))'"
1680 ));
1681 assert!(is_verbatim_output("python3 -c 'print(42)'"));
1682 assert!(is_verbatim_output(
1683 "node -e 'console.log(JSON.stringify({a:1}))'"
1684 ));
1685 assert!(is_verbatim_output("node --eval 'console.log(1)'"));
1686 assert!(is_verbatim_output("ruby -e 'puts 42'"));
1687 assert!(is_verbatim_output("perl -e 'print 42'"));
1688 assert!(is_verbatim_output("php -r 'echo json_encode([1,2,3]);'"));
1689 }
1690
1691 #[test]
1692 fn language_scripts_not_verbatim() {
1693 assert!(!is_verbatim_output("python script.py"));
1694 assert!(!is_verbatim_output("node server.js"));
1695 assert!(!is_verbatim_output("ruby app.rb"));
1696 }
1697
1698 #[test]
1699 fn container_listings_are_verbatim() {
1700 assert!(is_verbatim_output("docker ps"));
1701 assert!(is_verbatim_output("docker ps -a"));
1702 assert!(is_verbatim_output("docker images"));
1703 assert!(is_verbatim_output("docker images -a"));
1704 assert!(is_verbatim_output("podman ps"));
1705 assert!(is_verbatim_output("podman images"));
1706 assert!(is_verbatim_output("kubectl get pods"));
1707 assert!(is_verbatim_output("kubectl get deployments -A"));
1708 assert!(is_verbatim_output("kubectl get svc --all-namespaces"));
1709 assert!(is_verbatim_output("k get pods"));
1710 assert!(is_verbatim_output("helm list"));
1711 assert!(is_verbatim_output("helm ls --all-namespaces"));
1712 assert!(is_verbatim_output("docker compose ps"));
1713 assert!(is_verbatim_output("docker-compose ps"));
1714 }
1715
1716 #[test]
1717 fn file_listings_are_verbatim() {
1718 assert!(is_verbatim_output("find . -name '*.rs'"));
1719 assert!(is_verbatim_output("find /var/log -type f"));
1720 assert!(is_verbatim_output("fd --extension rs"));
1721 assert!(is_verbatim_output("fdfind .rs src/"));
1722 assert!(is_verbatim_output("ls -la"));
1723 assert!(is_verbatim_output("ls -lah /tmp"));
1724 assert!(is_verbatim_output("exa -la"));
1725 assert!(is_verbatim_output("eza --long"));
1726 }
1727
1728 #[test]
1729 fn system_queries_are_verbatim() {
1730 assert!(is_verbatim_output("stat file.txt"));
1731 assert!(is_verbatim_output("wc -l file.txt"));
1732 assert!(is_verbatim_output("du -sh /var"));
1733 assert!(is_verbatim_output("df -h"));
1734 assert!(is_verbatim_output("free -m"));
1735 assert!(is_verbatim_output("uname -a"));
1736 assert!(is_verbatim_output("id"));
1737 assert!(is_verbatim_output("whoami"));
1738 assert!(is_verbatim_output("hostname"));
1739 assert!(is_verbatim_output("which python3"));
1740 assert!(is_verbatim_output("readlink -f ./link"));
1741 assert!(is_verbatim_output("sha256sum file.tar.gz"));
1742 assert!(is_verbatim_output("base64 file.bin"));
1743 assert!(is_verbatim_output("ip addr show"));
1744 assert!(is_verbatim_output("ss -tlnp"));
1745 }
1746
1747 #[test]
1748 fn pipe_tail_detection() {
1749 assert!(
1750 is_verbatim_output("kubectl get pods -o json | jq '.items[].metadata.name'"),
1751 "piped to jq must be verbatim"
1752 );
1753 assert!(
1754 is_verbatim_output("aws s3api list-objects --bucket x | jq '.Contents'"),
1755 "piped to jq must be verbatim"
1756 );
1757 assert!(
1758 is_verbatim_output("docker inspect web | head -50"),
1759 "piped to head must be verbatim"
1760 );
1761 assert!(
1762 is_verbatim_output("terraform state pull | jq '.resources'"),
1763 "piped to jq must be verbatim"
1764 );
1765 assert!(
1766 is_verbatim_output("echo hello | wc -l"),
1767 "piped to wc (system query) should be verbatim"
1768 );
1769 }
1770
1771 #[test]
1772 fn build_commands_not_verbatim() {
1773 assert!(!is_verbatim_output("cargo build"));
1774 assert!(!is_verbatim_output("npm run build"));
1775 assert!(!is_verbatim_output("make"));
1776 assert!(!is_verbatim_output("docker build ."));
1777 assert!(!is_verbatim_output("go build ./..."));
1778 assert!(!is_verbatim_output("cargo test"));
1779 assert!(!is_verbatim_output("pytest"));
1780 assert!(!is_verbatim_output("npm install"));
1781 assert!(!is_verbatim_output("pip install requests"));
1782 assert!(!is_verbatim_output("terraform plan"));
1783 assert!(!is_verbatim_output("terraform apply"));
1784 }
1785
1786 #[test]
1787 fn cloud_cli_queries_are_verbatim() {
1788 assert!(is_verbatim_output("aws sts get-caller-identity"));
1789 assert!(is_verbatim_output("aws ec2 describe-instances"));
1790 assert!(is_verbatim_output(
1791 "aws s3api list-objects --bucket my-bucket"
1792 ));
1793 assert!(is_verbatim_output("aws iam list-users"));
1794 assert!(is_verbatim_output("aws ecs describe-tasks --cluster x"));
1795 assert!(is_verbatim_output("aws rds describe-db-instances"));
1796 assert!(is_verbatim_output("gcloud compute instances list"));
1797 assert!(is_verbatim_output("gcloud projects describe my-project"));
1798 assert!(is_verbatim_output("gcloud iam roles list"));
1799 assert!(is_verbatim_output("gcloud container clusters list"));
1800 assert!(is_verbatim_output("az vm list"));
1801 assert!(is_verbatim_output("az account show"));
1802 assert!(is_verbatim_output("az network nsg list"));
1803 assert!(is_verbatim_output("az aks show --name mycluster"));
1804 }
1805
1806 #[test]
1807 fn cloud_cli_mutations_not_verbatim() {
1808 assert!(!is_verbatim_output("aws configure"));
1809 assert!(!is_verbatim_output("gcloud auth login"));
1810 assert!(!is_verbatim_output("az login"));
1811 assert!(!is_verbatim_output("gcloud app deploy"));
1812 }
1813
1814 #[test]
1815 fn package_manager_info_is_verbatim() {
1816 assert!(is_verbatim_output("npm list"));
1817 assert!(is_verbatim_output("npm ls --all"));
1818 assert!(is_verbatim_output("npm info react"));
1819 assert!(is_verbatim_output("npm view react versions"));
1820 assert!(is_verbatim_output("npm outdated"));
1821 assert!(is_verbatim_output("npm audit"));
1822 assert!(is_verbatim_output("yarn list"));
1823 assert!(is_verbatim_output("yarn info react"));
1824 assert!(is_verbatim_output("yarn why react"));
1825 assert!(is_verbatim_output("yarn audit"));
1826 assert!(is_verbatim_output("pnpm list"));
1827 assert!(is_verbatim_output("pnpm why react"));
1828 assert!(is_verbatim_output("pnpm outdated"));
1829 assert!(is_verbatim_output("pip list"));
1830 assert!(is_verbatim_output("pip show requests"));
1831 assert!(is_verbatim_output("pip freeze"));
1832 assert!(is_verbatim_output("pip3 list"));
1833 assert!(is_verbatim_output("gem list"));
1834 assert!(is_verbatim_output("gem info rails"));
1835 assert!(is_verbatim_output("cargo metadata"));
1836 assert!(is_verbatim_output("cargo tree"));
1837 assert!(is_verbatim_output("go list ./..."));
1838 assert!(is_verbatim_output("go version"));
1839 assert!(is_verbatim_output("composer show"));
1840 assert!(is_verbatim_output("composer outdated"));
1841 assert!(is_verbatim_output("brew list"));
1842 assert!(is_verbatim_output("brew info node"));
1843 assert!(is_verbatim_output("brew deps node"));
1844 assert!(is_verbatim_output("apt list --installed"));
1845 assert!(is_verbatim_output("apt show nginx"));
1846 assert!(is_verbatim_output("dpkg -l"));
1847 assert!(is_verbatim_output("dpkg -s nginx"));
1848 }
1849
1850 #[test]
1851 fn package_manager_install_not_verbatim() {
1852 assert!(!is_verbatim_output("npm install"));
1853 assert!(!is_verbatim_output("yarn add react"));
1854 assert!(!is_verbatim_output("pip install requests"));
1855 assert!(!is_verbatim_output("cargo build"));
1856 assert!(!is_verbatim_output("go build"));
1857 assert!(!is_verbatim_output("brew install node"));
1858 assert!(!is_verbatim_output("apt install nginx"));
1859 }
1860
1861 #[test]
1862 fn version_and_help_are_verbatim() {
1863 assert!(is_verbatim_output("node --version"));
1864 assert!(is_verbatim_output("python3 --version"));
1865 assert!(is_verbatim_output("rustc -V"));
1866 assert!(is_verbatim_output("docker version"));
1867 assert!(is_verbatim_output("git --version"));
1868 assert!(is_verbatim_output("cargo --help"));
1869 assert!(is_verbatim_output("docker help"));
1870 assert!(is_verbatim_output("git -h"));
1871 assert!(is_verbatim_output("npm help install"));
1872 }
1873
1874 #[test]
1875 fn version_flag_needs_binary_context() {
1876 assert!(!is_verbatim_output("--version"));
1877 assert!(
1878 !is_verbatim_output("some command with --version and other args too"),
1879 "commands with 4+ tokens should not match version check"
1880 );
1881 }
1882
1883 #[test]
1884 fn config_viewers_are_verbatim() {
1885 assert!(is_verbatim_output("git config --list"));
1886 assert!(is_verbatim_output("git config --global --list"));
1887 assert!(is_verbatim_output("git config user.email"));
1888 assert!(is_verbatim_output("npm config list"));
1889 assert!(is_verbatim_output("npm config get registry"));
1890 assert!(is_verbatim_output("yarn config list"));
1891 assert!(is_verbatim_output("pip config list"));
1892 assert!(is_verbatim_output("rustup show"));
1893 assert!(is_verbatim_output("rustup target list"));
1894 assert!(is_verbatim_output("docker context ls"));
1895 assert!(is_verbatim_output("kubectl config view"));
1896 assert!(is_verbatim_output("kubectl config get-contexts"));
1897 assert!(is_verbatim_output("kubectl config current-context"));
1898 }
1899
1900 #[test]
1901 fn config_setters_not_verbatim() {
1902 assert!(!is_verbatim_output("git config --set user.name foo"));
1903 assert!(!is_verbatim_output("git config --unset user.name"));
1904 }
1905
1906 #[test]
1907 fn log_viewers_are_verbatim() {
1908 assert!(is_verbatim_output("journalctl -u nginx"));
1909 assert!(is_verbatim_output("journalctl --since '1 hour ago'"));
1910 assert!(is_verbatim_output("dmesg"));
1911 assert!(is_verbatim_output("dmesg --level=err"));
1912 assert!(is_verbatim_output("docker logs mycontainer"));
1913 assert!(is_verbatim_output("docker logs --tail 100 web"));
1914 assert!(is_verbatim_output("kubectl logs pod/web"));
1915 assert!(is_verbatim_output("docker compose logs web"));
1916 }
1917
1918 #[test]
1919 fn follow_logs_not_verbatim() {
1920 assert!(!is_verbatim_output("journalctl -f"));
1921 assert!(!is_verbatim_output("journalctl --follow -u nginx"));
1922 assert!(!is_verbatim_output("dmesg -w"));
1923 assert!(!is_verbatim_output("dmesg --follow"));
1924 assert!(!is_verbatim_output("docker logs -f web"));
1925 assert!(!is_verbatim_output("kubectl logs -f pod/web"));
1926 assert!(!is_verbatim_output("docker compose logs -f"));
1927 }
1928
1929 #[test]
1930 fn archive_listings_are_verbatim() {
1931 assert!(is_verbatim_output("tar -tf archive.tar.gz"));
1932 assert!(is_verbatim_output("tar tf archive.tar"));
1933 assert!(is_verbatim_output("unzip -l archive.zip"));
1934 assert!(is_verbatim_output("zipinfo archive.zip"));
1935 assert!(is_verbatim_output("lsar archive.7z"));
1936 }
1937
1938 #[test]
1939 fn clipboard_tools_are_verbatim() {
1940 assert!(is_verbatim_output("pbpaste"));
1941 assert!(is_verbatim_output("wl-paste"));
1942 assert!(is_verbatim_output("xclip -o"));
1943 assert!(is_verbatim_output("xclip -selection clipboard -o"));
1944 assert!(is_verbatim_output("xsel -o"));
1945 assert!(is_verbatim_output("xsel --output"));
1946 }
1947
1948 #[test]
1949 fn git_data_commands_are_verbatim() {
1950 assert!(is_verbatim_output("git remote -v"));
1951 assert!(is_verbatim_output("git remote show origin"));
1952 assert!(is_verbatim_output("git config --list"));
1953 assert!(is_verbatim_output("git rev-parse HEAD"));
1954 assert!(is_verbatim_output("git rev-parse --show-toplevel"));
1955 assert!(is_verbatim_output("git ls-files"));
1956 assert!(is_verbatim_output("git ls-tree HEAD"));
1957 assert!(is_verbatim_output("git ls-remote origin"));
1958 assert!(is_verbatim_output("git shortlog -sn"));
1959 assert!(is_verbatim_output("git for-each-ref --format='%(refname)'"));
1960 assert!(is_verbatim_output("git cat-file -p HEAD"));
1961 assert!(is_verbatim_output("git describe --tags"));
1962 assert!(is_verbatim_output("git merge-base main feature"));
1963 }
1964
1965 #[test]
1966 fn git_mutations_not_verbatim_via_git_data() {
1967 assert!(!super::is_git_data_command("git commit -m 'fix'"));
1968 assert!(!super::is_git_data_command("git push"));
1969 assert!(!super::is_git_data_command("git pull"));
1970 assert!(!super::is_git_data_command("git fetch"));
1971 assert!(!super::is_git_data_command("git add ."));
1972 assert!(!super::is_git_data_command("git rebase main"));
1973 assert!(!super::is_git_data_command("git cherry-pick abc123"));
1974 }
1975
1976 #[test]
1977 fn task_dry_run_is_verbatim() {
1978 assert!(is_verbatim_output("make -n build"));
1979 assert!(is_verbatim_output("make --dry-run"));
1980 assert!(is_verbatim_output("ansible-playbook --check site.yml"));
1981 assert!(is_verbatim_output(
1982 "ansible-playbook --diff --check site.yml"
1983 ));
1984 }
1985
1986 #[test]
1987 fn task_execution_not_verbatim() {
1988 assert!(!is_verbatim_output("make build"));
1989 assert!(!is_verbatim_output("make"));
1990 assert!(!is_verbatim_output("ansible-playbook site.yml"));
1991 }
1992
1993 #[test]
1994 fn env_dump_is_verbatim() {
1995 assert!(is_verbatim_output("env"));
1996 assert!(is_verbatim_output("printenv"));
1997 assert!(is_verbatim_output("printenv PATH"));
1998 assert!(is_verbatim_output("locale"));
1999 }
2000
2001 #[test]
2002 fn curl_json_output_preserved() {
2003 let json = r#"{"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}],"total":2,"page":1}"#;
2004 let result = compress_if_beneficial("curl https://api.example.com/users", json);
2005 assert!(
2006 result.contains("alice@example.com"),
2007 "curl JSON data must be preserved verbatim, got: {result}"
2008 );
2009 assert!(
2010 result.contains(r#""name":"Bob""#),
2011 "curl JSON data must be preserved verbatim, got: {result}"
2012 );
2013 }
2014
2015 #[test]
2016 fn curl_html_output_preserved() {
2017 let html = "<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Hello World</h1><p>Some important content here that should not be summarized.</p></body></html>";
2018 let result = compress_if_beneficial("curl https://example.com", html);
2019 assert!(
2020 result.contains("Hello World"),
2021 "curl HTML content must be preserved, got: {result}"
2022 );
2023 assert!(
2024 result.contains("important content"),
2025 "curl HTML content must be preserved, got: {result}"
2026 );
2027 }
2028
2029 #[test]
2030 fn curl_headers_preserved() {
2031 let headers = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Request-Id: abc-123\r\nX-RateLimit-Remaining: 59\r\nContent-Length: 1234\r\nServer: nginx\r\nDate: Mon, 01 Jan 2024 00:00:00 GMT\r\n\r\n";
2032 let result = compress_if_beneficial("curl -I https://api.example.com", headers);
2033 assert!(
2034 result.contains("X-Request-Id: abc-123"),
2035 "curl headers must be preserved, got: {result}"
2036 );
2037 assert!(
2038 result.contains("X-RateLimit-Remaining"),
2039 "curl headers must be preserved, got: {result}"
2040 );
2041 }
2042
2043 #[test]
2044 fn cat_output_preserved() {
2045 let content = r#"{
2046 "name": "lean-ctx",
2047 "version": "3.5.16",
2048 "description": "Context Runtime for AI Agents",
2049 "main": "index.js",
2050 "scripts": {
2051 "build": "cargo build --release",
2052 "test": "cargo test"
2053 }
2054}"#;
2055 let result = compress_if_beneficial("cat package.json", content);
2056 assert!(
2057 result.contains(r#""version": "3.5.16""#),
2058 "cat output must be preserved, got: {result}"
2059 );
2060 }
2061
2062 #[test]
2063 fn jq_output_preserved() {
2064 let json = r#"[
2065 {"id": 1, "status": "active", "name": "Alice"},
2066 {"id": 2, "status": "inactive", "name": "Bob"},
2067 {"id": 3, "status": "active", "name": "Charlie"}
2068]"#;
2069 let result =
2070 compress_if_beneficial("jq '.[] | select(.status==\"active\")' data.json", json);
2071 assert!(
2072 result.contains("Charlie"),
2073 "jq output must be preserved, got: {result}"
2074 );
2075 }
2076
2077 #[test]
2078 fn wget_output_preserved() {
2079 let content = r#"{"key": "value", "data": [1, 2, 3]}"#;
2080 let result = compress_if_beneficial("wget -qO- https://api.example.com/data", content);
2081 assert!(
2082 result.contains(r#""data": [1, 2, 3]"#),
2083 "wget data output must be preserved, got: {result}"
2084 );
2085 }
2086
2087 #[test]
2088 fn large_curl_output_gets_truncated_not_destroyed() {
2089 let mut json = String::from("[");
2090 for i in 0..500 {
2091 if i > 0 {
2092 json.push(',');
2093 }
2094 json.push_str(&format!(
2095 r#"{{"id":{i},"name":"user_{i}","email":"user{i}@example.com","role":"admin"}}"#
2096 ));
2097 }
2098 json.push(']');
2099 let result = compress_if_beneficial("curl https://api.example.com/all-users", &json);
2100 assert!(
2101 result.contains("user_0"),
2102 "first items must be preserved in truncated output, got len: {}",
2103 result.len()
2104 );
2105 if result.contains("lines omitted") {
2106 assert!(
2107 result.contains("verbatim truncated"),
2108 "must mark as verbatim truncated, got: {result}"
2109 );
2110 }
2111 }
2112}
2113
2114#[cfg(test)]
2115mod structural_output_tests {
2116 use super::has_structural_output;
2117
2118 #[test]
2119 fn git_diff_is_structural() {
2120 assert!(has_structural_output("git diff"));
2121 assert!(has_structural_output("git diff --cached"));
2122 assert!(has_structural_output("git diff --staged"));
2123 assert!(has_structural_output("git diff HEAD~1"));
2124 assert!(has_structural_output("git diff main..feature"));
2125 assert!(has_structural_output("git diff -- src/main.rs"));
2126 }
2127
2128 #[test]
2129 fn git_show_is_structural() {
2130 assert!(has_structural_output("git show"));
2131 assert!(has_structural_output("git show HEAD"));
2132 assert!(has_structural_output("git show abc1234"));
2133 assert!(has_structural_output("git show stash@{0}"));
2134 }
2135
2136 #[test]
2137 fn git_blame_is_structural() {
2138 assert!(has_structural_output("git blame src/main.rs"));
2139 assert!(has_structural_output("git blame -L 10,20 file.rs"));
2140 }
2141
2142 #[test]
2143 fn git_with_flags_is_structural() {
2144 assert!(has_structural_output("git -C /tmp diff"));
2145 assert!(has_structural_output("git --git-dir /path diff HEAD"));
2146 assert!(has_structural_output("git -c core.pager=cat show abc"));
2147 }
2148
2149 #[test]
2150 fn case_insensitive() {
2151 assert!(has_structural_output("Git Diff"));
2152 assert!(has_structural_output("GIT DIFF --cached"));
2153 assert!(has_structural_output("git SHOW HEAD"));
2154 }
2155
2156 #[test]
2157 fn full_path_git_binary() {
2158 assert!(has_structural_output("/usr/bin/git diff"));
2159 assert!(has_structural_output("/usr/local/bin/git show HEAD"));
2160 }
2161
2162 #[test]
2163 fn standalone_diff_is_structural() {
2164 assert!(has_structural_output("diff file1.txt file2.txt"));
2165 assert!(has_structural_output("diff -u old.py new.py"));
2166 assert!(has_structural_output("diff -r dir1 dir2"));
2167 assert!(has_structural_output("/usr/bin/diff a b"));
2168 assert!(has_structural_output("colordiff file1 file2"));
2169 assert!(has_structural_output("icdiff old.rs new.rs"));
2170 assert!(has_structural_output("delta"));
2171 }
2172
2173 #[test]
2174 fn git_log_with_patch_is_structural() {
2175 assert!(has_structural_output("git log -p"));
2176 assert!(has_structural_output("git log --patch"));
2177 assert!(has_structural_output("git log -p HEAD~5"));
2178 assert!(has_structural_output("git log -p --stat"));
2179 assert!(has_structural_output("git log --patch --follow file.rs"));
2180 }
2181
2182 #[test]
2183 fn git_log_without_patch_not_structural() {
2184 assert!(!has_structural_output("git log"));
2185 assert!(!has_structural_output("git log --oneline"));
2186 assert!(!has_structural_output("git log --stat"));
2187 assert!(!has_structural_output("git log -n 5"));
2188 }
2189
2190 #[test]
2191 fn git_stash_show_is_structural() {
2192 assert!(has_structural_output("git stash show"));
2193 assert!(has_structural_output("git stash show -p"));
2194 assert!(has_structural_output("git stash show --patch"));
2195 assert!(has_structural_output("git stash show stash@{0}"));
2196 }
2197
2198 #[test]
2199 fn git_stash_without_show_not_structural() {
2200 assert!(!has_structural_output("git stash"));
2201 assert!(!has_structural_output("git stash list"));
2202 assert!(!has_structural_output("git stash pop"));
2203 assert!(!has_structural_output("git stash drop"));
2204 }
2205
2206 #[test]
2207 fn non_structural_git_commands() {
2208 assert!(!has_structural_output("git status"));
2209 assert!(!has_structural_output("git commit -m 'fix'"));
2210 assert!(!has_structural_output("git push"));
2211 assert!(!has_structural_output("git pull"));
2212 assert!(!has_structural_output("git branch"));
2213 assert!(!has_structural_output("git fetch"));
2214 assert!(!has_structural_output("git add ."));
2215 }
2216
2217 #[test]
2218 fn non_git_commands() {
2219 assert!(!has_structural_output("cargo build"));
2220 assert!(!has_structural_output("npm run build"));
2221 }
2222
2223 #[test]
2224 fn verbatim_commands_are_also_structural() {
2225 assert!(has_structural_output("ls -la"));
2226 assert!(has_structural_output("docker ps"));
2227 assert!(has_structural_output("curl https://api.example.com"));
2228 assert!(has_structural_output("cat file.txt"));
2229 assert!(has_structural_output("aws ec2 describe-instances"));
2230 assert!(has_structural_output("npm list"));
2231 assert!(has_structural_output("node --version"));
2232 assert!(has_structural_output("journalctl -u nginx"));
2233 assert!(has_structural_output("git remote -v"));
2234 assert!(has_structural_output("pbpaste"));
2235 assert!(has_structural_output("env"));
2236 }
2237
2238 #[test]
2239 fn git_diff_output_preserves_hunks() {
2240 let diff = "diff --git a/src/main.rs b/src/main.rs\n\
2241 index abc1234..def5678 100644\n\
2242 --- a/src/main.rs\n\
2243 +++ b/src/main.rs\n\
2244 @@ -1,5 +1,6 @@\n\
2245 fn main() {\n\
2246 + println!(\"hello\");\n\
2247 let x = 1;\n\
2248 let y = 2;\n\
2249 - let z = 3;\n\
2250 + let z = x + y;\n\
2251 }";
2252 let result = super::compress_if_beneficial("git diff", diff);
2253 assert!(
2254 result.contains("+ println!"),
2255 "must preserve added lines, got: {result}"
2256 );
2257 assert!(
2258 result.contains("- let z = 3;"),
2259 "must preserve removed lines, got: {result}"
2260 );
2261 assert!(
2262 result.contains("@@ -1,5 +1,6 @@"),
2263 "must preserve hunk headers, got: {result}"
2264 );
2265 }
2266
2267 #[test]
2268 fn git_diff_large_preserves_content() {
2269 let mut diff = String::new();
2270 diff.push_str("diff --git a/file.rs b/file.rs\n");
2271 diff.push_str("--- a/file.rs\n+++ b/file.rs\n");
2272 diff.push_str("@@ -1,100 +1,100 @@\n");
2273 for i in 0..80 {
2274 diff.push_str(&format!("+added line {i}: some actual code content\n"));
2275 diff.push_str(&format!("-removed line {i}: old code content\n"));
2276 }
2277 let result = super::compress_if_beneficial("git diff", &diff);
2278 assert!(
2279 result.contains("+added line 0"),
2280 "must preserve first added line, got len: {}",
2281 result.len()
2282 );
2283 assert!(
2284 result.contains("-removed line 0"),
2285 "must preserve first removed line, got len: {}",
2286 result.len()
2287 );
2288 }
2289}