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