1pub const PATTERN_ENGINE_VERSION: u32 = 1;
5
6pub mod ansible;
7pub mod artisan;
8pub mod aws;
9pub mod bazel;
10pub mod bun;
11pub mod cargo;
12pub mod clang;
13pub mod cmake;
14pub mod composer;
15pub mod curl;
16pub mod deno;
17pub mod deps_cmd;
18pub mod docker;
19pub mod dotnet;
20pub mod env_filter;
21pub mod eslint;
22pub mod fd;
23pub mod find;
24pub mod flutter;
25pub mod gh;
26pub mod git;
27pub mod glab;
28pub mod golang;
29pub mod grep;
30pub mod helm;
31pub mod json_schema;
32pub mod just;
33pub mod kubectl;
34pub mod log_dedup;
35pub mod ls;
36pub mod make;
37pub mod maven;
38pub mod mix;
39pub mod mypy;
40pub mod mysql;
41pub mod next_build;
42pub mod ninja;
43pub mod npm;
44pub mod php;
45pub mod pip;
46pub mod playwright;
47pub mod pnpm;
48pub mod poetry;
49pub mod prettier;
50pub mod prisma;
51pub mod psql;
52pub mod ruby;
53pub mod ruff;
54pub mod swift;
55pub mod sysinfo;
56pub mod systemd;
57pub mod terraform;
58pub mod test;
59pub mod typescript;
60pub mod wget;
61pub mod zig;
62
63use crate::core::tokens::count_tokens;
64
65fn is_passthrough_command(cmd: &str) -> bool {
66 let c = cmd.to_ascii_lowercase();
67 if c.starts_with("gh api ") || c.starts_with("gh api\t") {
68 return true;
69 }
70 if (c.starts_with("gh ") && c.contains("--log")) || c.contains("log-failed") {
71 return true;
72 }
73 false
74}
75
76pub fn compress_output(command: &str, output: &str) -> Option<String> {
77 if is_passthrough_command(command) {
78 return None;
79 }
80
81 let cleaned = crate::core::compressor::strip_ansi(output);
82 let output = if cleaned.len() < output.len() {
83 &cleaned
84 } else {
85 output
86 };
87
88 if let Some(engine) = crate::core::filters::FilterEngine::load() {
89 if let Some(filtered) = engine.apply(command, output) {
90 return shorter_only(filtered, output);
91 }
92 }
93
94 if let Some(compressed) = try_specific_pattern(command, output) {
95 if let Some(r) = shorter_only(compressed, output) {
96 return Some(r);
97 }
98 }
99
100 if let Some(r) = json_schema::compress(output) {
101 if let Some(r) = shorter_only(r, output) {
102 return Some(r);
103 }
104 }
105
106 if let Some(r) = log_dedup::compress(output) {
107 if let Some(r) = shorter_only(r, output) {
108 return Some(r);
109 }
110 }
111
112 if let Some(r) = test::compress(output) {
113 if let Some(r) = shorter_only(r, output) {
114 return Some(r);
115 }
116 }
117
118 if output.len() > 8000 {
119 return Some(truncate_large_output(command, output));
120 }
121
122 None
123}
124
125fn normalize_shell_tokens(text: &str) -> String {
127 text.split_whitespace().collect::<Vec<_>>().join(" ")
128}
129
130fn shorter_only(compressed: String, original: &str) -> Option<String> {
131 let orig_n = normalize_shell_tokens(original);
132 let comp_n = normalize_shell_tokens(&compressed);
133 let ct_c = count_tokens(&comp_n);
134 let ct_o = count_tokens(&orig_n);
135 if ct_c < ct_o || (ct_c == ct_o && comp_n.len() < orig_n.len()) {
136 Some(compressed)
137 } else {
138 None
139 }
140}
141
142fn truncate_large_output(command: &str, output: &str) -> String {
143 let lines: Vec<&str> = output.lines().collect();
144 let total = lines.len();
145 let size = output.len();
146 let head = 30.min(total);
147 let tail = 15.min(total.saturating_sub(head));
148
149 let mut result = String::with_capacity(4096);
150 let cmd_short = if command.len() > 60 {
151 &command[..60]
152 } else {
153 command
154 };
155 result.push_str(&format!("{cmd_short} ({size} bytes, {total} lines):\n"));
156 for line in lines.iter().take(head) {
157 if line.len() > 300 {
158 result.push_str(&line[..300]);
159 result.push_str("…\n");
160 } else {
161 result.push_str(line);
162 result.push('\n');
163 }
164 }
165 if total > head + tail {
166 result.push_str(&format!(
167 "\n[… {} lines omitted …]\n\n",
168 total - head - tail
169 ));
170 for line in lines.iter().skip(total - tail) {
171 if line.len() > 300 {
172 result.push_str(&line[..300]);
173 result.push_str("…\n");
174 } else {
175 result.push_str(line);
176 result.push('\n');
177 }
178 }
179 }
180 result
181}
182
183pub fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
184 let cl = cmd.to_ascii_lowercase();
185 let c = cl.as_str();
186
187 if c.starts_with("git ") {
188 return git::compress(c, output);
189 }
190 if c.starts_with("gh ") {
191 return gh::compress(c, output);
192 }
193 if c.starts_with("glab ") {
194 return glab::try_glab_pattern(c, output);
195 }
196 if c == "terraform" || c.starts_with("terraform ") {
197 return terraform::compress(c, output);
198 }
199 if c == "make" || c.starts_with("make ") {
200 return make::compress(c, output);
201 }
202 if c == "just" || c.starts_with("just ") {
203 return just::compress(c, output);
204 }
205 if c.starts_with("mvn ")
206 || c.starts_with("./mvnw ")
207 || c.starts_with("mvnw ")
208 || c.starts_with("gradle ")
209 || c.starts_with("./gradlew ")
210 || c.starts_with("gradlew ")
211 {
212 return maven::compress(c, output);
213 }
214 if c.starts_with("kubectl ") || c.starts_with("k ") {
215 return kubectl::compress(c, output);
216 }
217 if c.starts_with("helm ") {
218 return helm::compress(c, output);
219 }
220 if c.starts_with("pnpm ") {
221 return pnpm::compress(c, output);
222 }
223 if c.starts_with("bun ") || c.starts_with("bunx ") {
224 return bun::compress(c, output);
225 }
226 if c.starts_with("deno ") {
227 return deno::compress(c, output);
228 }
229 if c.starts_with("npm ") || c.starts_with("yarn ") {
230 return npm::compress(c, output);
231 }
232 if c.starts_with("cargo ") {
233 return cargo::compress(c, output);
234 }
235 if c.starts_with("docker ") || c.starts_with("docker-compose ") {
236 return docker::compress(c, output);
237 }
238 if c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip") {
239 return pip::compress(c, output);
240 }
241 if c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy ") {
242 return mypy::compress(c, output);
243 }
244 if c.starts_with("pytest") || c.starts_with("python -m pytest") {
245 return test::compress(output);
246 }
247 if c.starts_with("ruff ") {
248 return ruff::compress(c, output);
249 }
250 if c.starts_with("eslint")
251 || c.starts_with("npx eslint")
252 || c.starts_with("biome ")
253 || c.starts_with("stylelint")
254 {
255 return eslint::compress(c, output);
256 }
257 if c.starts_with("prettier") || c.starts_with("npx prettier") {
258 return prettier::compress(output);
259 }
260 if c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint") {
261 return golang::compress(c, output);
262 }
263 if c.starts_with("playwright")
264 || c.starts_with("npx playwright")
265 || c.starts_with("cypress")
266 || c.starts_with("npx cypress")
267 {
268 return playwright::compress(c, output);
269 }
270 if c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest") {
271 return test::compress(output);
272 }
273 if c.starts_with("next ")
274 || c.starts_with("npx next")
275 || c.starts_with("vite ")
276 || c.starts_with("npx vite")
277 || c.starts_with("vp ")
278 || c.starts_with("vite-plus ")
279 {
280 return next_build::compress(c, output);
281 }
282 if c.starts_with("tsc") || c.contains("typescript") {
283 return typescript::compress(output);
284 }
285 if c.starts_with("rubocop")
286 || c.starts_with("bundle ")
287 || c.starts_with("rake ")
288 || c.starts_with("rails test")
289 || c.starts_with("rspec")
290 {
291 return ruby::compress(c, output);
292 }
293 if c.starts_with("grep ") || c.starts_with("rg ") {
294 return grep::compress(output);
295 }
296 if c.starts_with("find ") {
297 return find::compress(output);
298 }
299 if c.starts_with("fd ") || c.starts_with("fdfind ") {
300 return fd::compress(output);
301 }
302 if c.starts_with("ls ") || c == "ls" {
303 return ls::compress(output);
304 }
305 if c.starts_with("curl ") {
306 return curl::compress_with_cmd(c, output);
307 }
308 if c.starts_with("wget ") {
309 return wget::compress(output);
310 }
311 if c == "env" || c.starts_with("env ") || c.starts_with("printenv") {
312 return env_filter::compress(output);
313 }
314 if c.starts_with("dotnet ") {
315 return dotnet::compress(c, output);
316 }
317 if c.starts_with("flutter ")
318 || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
319 {
320 return flutter::compress(c, output);
321 }
322 if c.starts_with("poetry ")
323 || c.starts_with("uv sync")
324 || (c.starts_with("uv ") && c.contains("pip install"))
325 || c.starts_with("conda ")
326 || c.starts_with("mamba ")
327 || c.starts_with("pipx ")
328 {
329 return poetry::compress(c, output);
330 }
331 if c.starts_with("aws ") {
332 return aws::compress(c, output);
333 }
334 if c.starts_with("psql ") || c.starts_with("pg_") {
335 return psql::compress(c, output);
336 }
337 if c.starts_with("mysql ") || c.starts_with("mariadb ") {
338 return mysql::compress(c, output);
339 }
340 if c.starts_with("prisma ") || c.starts_with("npx prisma") {
341 return prisma::compress(c, output);
342 }
343 if c.starts_with("swift ") {
344 return swift::compress(c, output);
345 }
346 if c.starts_with("zig ") {
347 return zig::compress(c, output);
348 }
349 if c.starts_with("cmake ") || c.starts_with("ctest") {
350 return cmake::compress(c, output);
351 }
352 if c.starts_with("ninja") {
353 return ninja::compress(c, output);
354 }
355 if c.starts_with("ansible") || c.starts_with("ansible-playbook") {
356 return ansible::compress(c, output);
357 }
358 if c.starts_with("composer ") {
359 return composer::compress(c, output);
360 }
361 if c.starts_with("php artisan") || c.starts_with("artisan ") {
362 return artisan::compress(c, output);
363 }
364 if c.starts_with("./vendor/bin/pest") || c.starts_with("pest ") {
365 return artisan::compress("php artisan test", output);
366 }
367 if c.starts_with("mix ") || c.starts_with("iex ") {
368 return mix::compress(c, output);
369 }
370 if c.starts_with("bazel ") || c.starts_with("blaze ") {
371 return bazel::compress(c, output);
372 }
373 if c.starts_with("systemctl ") || c.starts_with("journalctl") {
374 return systemd::compress(c, output);
375 }
376 if c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest") {
377 return test::compress(output);
378 }
379 if c.starts_with("mocha") || c.starts_with("npx mocha") {
380 return test::compress(output);
381 }
382 if c.starts_with("tofu ") {
383 return terraform::compress(c, output);
384 }
385 if c.starts_with("ps ") || c == "ps" {
386 return sysinfo::compress_ps(output);
387 }
388 if c.starts_with("df ") || c == "df" {
389 return sysinfo::compress_df(output);
390 }
391 if c.starts_with("du ") || c == "du" {
392 return sysinfo::compress_du(output);
393 }
394 if c.starts_with("ping ") {
395 return sysinfo::compress_ping(output);
396 }
397 if c.starts_with("jq ") || c == "jq" {
398 return json_schema::compress(output);
399 }
400 if c.starts_with("hadolint") {
401 return eslint::compress(c, output);
402 }
403 if c.starts_with("yamllint") || c.starts_with("npx yamllint") {
404 return eslint::compress(c, output);
405 }
406 if c.starts_with("markdownlint") || c.starts_with("npx markdownlint") {
407 return eslint::compress(c, output);
408 }
409 if c.starts_with("oxlint") || c.starts_with("npx oxlint") {
410 return eslint::compress(c, output);
411 }
412 if c.starts_with("pyright") || c.starts_with("basedpyright") {
413 return mypy::compress(c, output);
414 }
415 if c.starts_with("turbo ") || c.starts_with("npx turbo") {
416 return npm::compress(c, output);
417 }
418 if c.starts_with("nx ") || c.starts_with("npx nx") {
419 return npm::compress(c, output);
420 }
421 if c.starts_with("clang++ ") || c.starts_with("clang ") {
422 return clang::compress(c, output);
423 }
424 if c.starts_with("gcc ")
425 || c.starts_with("g++ ")
426 || c.starts_with("cc ")
427 || c.starts_with("c++ ")
428 {
429 return cmake::compress(c, output);
430 }
431
432 None
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn routes_git_commands() {
441 let output = "On branch main\nnothing to commit";
442 assert!(compress_output("git status", output).is_some());
443 }
444
445 #[test]
446 fn routes_cargo_commands() {
447 let output = " Compiling lean-ctx v2.1.1\n Finished `release` profile [optimized] target(s) in 30.5s";
448 assert!(compress_output("cargo build --release", output).is_some());
449 }
450
451 #[test]
452 fn routes_npm_commands() {
453 let output = "added 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities";
454 assert!(compress_output("npm install", output).is_some());
455 }
456
457 #[test]
458 fn routes_docker_commands() {
459 let output = "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES";
460 assert!(compress_output("docker ps", output).is_some());
461 }
462
463 #[test]
464 fn routes_mypy_commands() {
465 let output = "src/main.py:10: error: Missing return [return]\nFound 1 error in 1 file (checked 3 source files)";
466 assert!(compress_output("mypy .", output).is_some());
467 assert!(compress_output("python -m mypy src/", output).is_some());
468 }
469
470 #[test]
471 fn routes_pytest_commands() {
472 let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
473 assert!(compress_output("pytest", output).is_some());
474 assert!(compress_output("python -m pytest tests/", output).is_some());
475 }
476
477 #[test]
478 fn unknown_command_returns_none() {
479 assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
480 }
481
482 #[test]
483 fn case_insensitive_routing() {
484 let output = "On branch main\nnothing to commit";
485 assert!(compress_output("Git Status", output).is_some());
486 assert!(compress_output("GIT STATUS", output).is_some());
487 }
488
489 #[test]
490 fn routes_vp_and_vite_plus() {
491 let output = " VITE v5.0.0 ready in 200 ms\n\n -> Local: http://localhost:5173/\n -> Network: http://192.168.1.2:5173/";
492 assert!(compress_output("vp build", output).is_some());
493 assert!(compress_output("vite-plus build", output).is_some());
494 }
495
496 #[test]
497 fn routes_bunx_commands() {
498 let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
499 let result = compress_output("bunx test", output);
500 assert!(
501 result.is_some(),
502 "bunx should compress when output is large enough"
503 );
504 assert!(result.unwrap().contains("bun test: 1 passed"));
505 }
506
507 #[test]
508 fn routes_deno_task() {
509 let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
510 assert!(try_specific_pattern("deno task dev", output).is_some());
511 }
512
513 #[test]
514 fn routes_jest_commands() {
515 let output = "PASS tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests: 5 passed, 5 total\nTime: 2.5 s";
516 assert!(try_specific_pattern("jest", output).is_some());
517 assert!(try_specific_pattern("npx jest --coverage", output).is_some());
518 }
519
520 #[test]
521 fn routes_mocha_commands() {
522 let output = " 3 passing (50ms)\n 1 failing\n\n 1) Array #indexOf():\n Error: expected -1 to equal 0";
523 assert!(try_specific_pattern("mocha", output).is_some());
524 assert!(try_specific_pattern("npx mocha tests/", output).is_some());
525 }
526
527 #[test]
528 fn routes_tofu_commands() {
529 let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
530 assert!(try_specific_pattern("tofu init", output).is_some());
531 }
532
533 #[test]
534 fn routes_ps_commands() {
535 let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
536 for i in 0..20 {
537 lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
538 }
539 let output = lines.join("\n");
540 assert!(try_specific_pattern("ps aux", &output).is_some());
541 }
542
543 #[test]
544 fn routes_ping_commands() {
545 let output = "PING google.com (1.2.3.4): 56 data bytes\n64 bytes from 1.2.3.4: icmp_seq=0 ttl=116 time=12ms\n3 packets transmitted, 3 packets received, 0.0% packet loss\nrtt min/avg/max/stddev = 11/12/13/1 ms";
546 assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
547 }
548
549 #[test]
550 fn routes_jq_to_json_schema() {
551 let output = "{\"name\": \"test\", \"version\": \"1.0\", \"items\": [{\"id\": 1}, {\"id\": 2}, {\"id\": 3}, {\"id\": 4}, {\"id\": 5}, {\"id\": 6}, {\"id\": 7}, {\"id\": 8}, {\"id\": 9}, {\"id\": 10}]}";
552 assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
553 }
554
555 #[test]
556 fn routes_linting_tools() {
557 let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
558 assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
559 assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
560 assert!(try_specific_pattern("pyright src/", lint_output).is_some());
561 assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
562 }
563
564 #[test]
565 fn routes_fd_commands() {
566 let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
567 assert!(try_specific_pattern("fd --extension rs", output).is_some());
568 assert!(try_specific_pattern("fdfind .rs", output).is_some());
569 }
570
571 #[test]
572 fn routes_just_commands() {
573 let output = "Available recipes:\n build\n test\n lint\n";
574 assert!(try_specific_pattern("just --list", output).is_some());
575 assert!(try_specific_pattern("just build", output).is_some());
576 }
577
578 #[test]
579 fn routes_ninja_commands() {
580 let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
581 assert!(try_specific_pattern("ninja", output).is_some());
582 assert!(try_specific_pattern("ninja -j4", output).is_some());
583 }
584
585 #[test]
586 fn routes_clang_commands() {
587 let output =
588 "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
589 assert!(try_specific_pattern("clang src/main.c", output).is_some());
590 assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
591 }
592
593 #[test]
594 fn routes_cargo_run() {
595 let output = " Compiling foo v0.1.0\n Finished `dev` profile\nHello, world!";
596 assert!(try_specific_pattern("cargo run", output).is_some());
597 }
598
599 #[test]
600 fn routes_cargo_bench() {
601 let output = " Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
602 assert!(try_specific_pattern("cargo bench", output).is_some());
603 }
604
605 #[test]
606 fn routes_build_tools() {
607 let build_output = " Compiling foo v0.1.0\n Finished release [optimized]";
608 assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
609 assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
610 }
611
612 #[test]
613 fn routes_monorepo_tools() {
614 let output = "npm warn deprecated inflight@1.0.6\nnpm warn deprecated rimraf@3.0.2\nadded 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities";
615 assert!(try_specific_pattern("turbo install", output).is_some());
616 assert!(try_specific_pattern("nx install", output).is_some());
617 }
618
619 #[test]
620 fn gh_api_passthrough_never_compresses() {
621 let huge = "line\n".repeat(5000);
622 assert!(
623 compress_output("gh api repos/owner/repo/actions/jobs/123/logs", &huge).is_none(),
624 "gh api must never be compressed, even for large output"
625 );
626 assert!(compress_output("gh api repos/owner/repo/actions/runs/123/logs", &huge).is_none());
627 }
628
629 #[test]
630 fn gh_log_flags_passthrough() {
631 let huge = "line\n".repeat(5000);
632 assert!(compress_output("gh run view 123 --log-failed", &huge).is_none());
633 assert!(compress_output("gh run view 123 --log", &huge).is_none());
634 }
635
636 #[test]
637 fn gh_structured_commands_still_compress() {
638 let output = "On branch main\nnothing to commit";
639 assert!(try_specific_pattern("gh pr list", output).is_some());
640 assert!(try_specific_pattern("gh run list", output).is_some());
641 }
642}