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