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