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