1pub const PATTERN_ENGINE_VERSION: u32 = 2;
8
9pub mod alembic;
10pub mod ansible;
11pub mod argocd;
12pub mod artisan;
13pub mod aws;
14pub mod bazel;
15pub mod buf;
16pub mod bun;
17pub mod cargo;
18pub mod cargo_diagnostics;
19pub mod clang;
20pub mod cmake;
21pub mod composer;
22pub mod cosign;
23pub mod curl;
24pub mod dbt;
25pub mod deno;
26pub mod deploy;
27pub mod deps_cmd;
28pub mod docker;
29pub mod dotnet;
30pub mod env_filter;
31pub mod eslint;
32pub mod fd;
33pub mod find;
34pub mod flutter;
35pub mod flyway;
36pub mod gem;
37pub mod gh;
38pub mod git;
39pub mod glab;
40pub mod golang;
41pub mod grep;
42pub mod grype;
43pub mod helm;
44pub mod jj;
45pub mod json_schema;
46pub mod just;
47pub mod kubectl;
48pub mod linkerd;
49pub mod log_dedup;
50pub mod ls;
51pub mod make;
52pub mod maven;
53pub mod mise;
54pub mod mix;
55pub mod mlflow;
56pub mod mypy;
57pub mod mysql;
58pub mod next_build;
59pub mod ninja;
60pub mod npm;
61pub mod ollama;
62pub mod php;
63pub mod pip;
64pub mod playwright;
65pub mod pnpm;
66pub mod poetry;
67pub mod prettier;
68pub mod prisma;
69pub mod psql;
70pub mod pulumi;
71pub mod pytest;
72pub mod ruby;
73pub mod ruff;
74pub mod semgrep;
75pub mod spark;
76pub mod swift;
77pub mod swiftlint;
78pub mod syft;
79pub mod sysinfo;
80pub mod systemd;
81pub mod terraform;
82pub mod test;
83pub mod trivy;
84pub mod typescript;
85pub mod wget;
86pub mod zig;
87
88use crate::core::tokens::count_tokens;
89
90pub fn compress_output(command: &str, output: &str) -> Option<String> {
91 let policy = crate::shell::output_policy::classify(command, &[]);
96 if policy.is_protected() {
97 return None;
98 }
99
100 let stripped;
101 let clean_output = {
102 let s = crate::core::compressor::strip_ansi(output);
103 if s.len() < output.len() {
104 stripped = s;
105 &stripped
106 } else {
107 output
108 }
109 };
110
111 if let Some(engine) = crate::core::filters::FilterEngine::load()
112 && let Some(filtered) = engine.apply(command, clean_output)
113 {
114 return shorter_only(filtered, output);
115 }
116
117 if has_vcs_owner(command) {
126 return try_specific_pattern(command, clean_output)
133 .filter(|c| !c.trim().is_empty())
134 .filter(|c| !inflates_tokens(c, output));
135 }
136
137 if let Some(compressed) = try_specific_pattern(command, clean_output)
138 && let Some(r) = shorter_only(compressed, output)
139 {
140 return Some(r);
141 }
142
143 if let Some(r) = json_schema::compress(clean_output)
144 && let Some(r) = shorter_only(r, output)
145 {
146 return Some(r);
147 }
148
149 if let Some(r) = log_dedup::compress(clean_output)
150 && let Some(r) = shorter_only(r, output)
151 {
152 return Some(r);
153 }
154
155 if let Some(r) = test::compress(clean_output)
156 && let Some(r) = shorter_only(r, output)
157 {
158 return Some(r);
159 }
160
161 None
162}
163
164pub(crate) fn has_vcs_owner(command: &str) -> bool {
168 let c = command.trim_start().to_ascii_lowercase();
169 c.starts_with("git ")
170 || c.starts_with("jj ")
171 || c.starts_with("gh ")
172 || c.starts_with("glab ")
173 || c.starts_with("hg ")
174}
175
176fn normalize_shell_tokens(text: &str) -> String {
178 text.split_whitespace().collect::<Vec<_>>().join(" ")
179}
180
181fn inflates_tokens(compressed: &str, original: &str) -> bool {
186 count_tokens(compressed) > count_tokens(original)
187}
188
189fn shorter_only(compressed: String, original: &str) -> Option<String> {
190 let orig_n = normalize_shell_tokens(original);
191 let comp_n = normalize_shell_tokens(&compressed);
192 let ct_c = count_tokens(&comp_n);
193 let ct_o = count_tokens(&orig_n);
194 if ct_c < ct_o || (ct_c == ct_o && comp_n.len() < orig_n.len()) {
195 Some(compressed)
196 } else {
197 None
198 }
199}
200
201type PatternMatcher = fn(&str) -> bool;
202type PatternHandler = fn(&str, &str) -> Option<String>;
203
204const PATTERNS: &[(PatternMatcher, PatternHandler)] = &[
211 (
212 |c| c.starts_with("git "),
213 |c, output| git::compress(c, output),
214 ),
215 (
216 |c| c.starts_with("gh "),
217 |c, output| gh::compress(c, output),
218 ),
219 (
220 |c| c.starts_with("glab "),
221 |c, output| glab::try_glab_pattern(c, output),
222 ),
223 (
224 |c| c == "terraform" || c.starts_with("terraform "),
225 |c, output| terraform::compress(c, output),
226 ),
227 (
228 |c| c == "make" || c.starts_with("make "),
229 |c, output| make::compress(c, output),
230 ),
231 (
232 |c| c == "just" || c.starts_with("just "),
233 |c, output| just::compress(c, output),
234 ),
235 (
236 |c| {
237 c.starts_with("mvn ")
238 || c.starts_with("./mvnw ")
239 || c.starts_with("mvnw ")
240 || c.starts_with("gradle ")
241 || c.starts_with("./gradlew ")
242 || c.starts_with("gradlew ")
243 },
244 |c, output| maven::compress(c, output),
245 ),
246 (
247 |c| c.starts_with("kubectl ") || c.starts_with("k "),
248 |c, output| kubectl::compress(c, output),
249 ),
250 (
251 |c| c.starts_with("helm "),
252 |c, output| helm::compress(c, output),
253 ),
254 (
255 |c| c.starts_with("pnpm "),
256 |c, output| pnpm::compress(c, output),
257 ),
258 (
259 |c| c.starts_with("bun ") || c.starts_with("bunx "),
260 |c, output| bun::compress(c, output),
261 ),
262 (
263 |c| c.starts_with("deno "),
264 |c, output| deno::compress(c, output),
265 ),
266 (
267 |c| c.starts_with("npm ") || c.starts_with("yarn "),
268 |c, output| npm::compress(c, output),
269 ),
270 (
271 |c| c.starts_with("cargo "),
272 |c, output| cargo::compress(c, output),
273 ),
274 (
275 |c| c.starts_with("docker ") || c.starts_with("docker-compose "),
276 |c, output| docker::compress(c, output),
277 ),
278 (
279 |c| c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip"),
280 |c, output| pip::compress(c, output),
281 ),
282 (
283 |c| c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy "),
284 |c, output| mypy::compress(c, output),
285 ),
286 (
287 |c| c.starts_with("pytest") || c.starts_with("python -m pytest"),
288 |c, output| pytest::compress(c, output).or_else(|| test::compress(output)),
289 ),
290 (
291 |c| c.starts_with("ruff "),
292 |c, output| ruff::compress(c, output),
293 ),
294 (
295 |c| {
296 c.starts_with("eslint")
297 || c.starts_with("npx eslint")
298 || c.starts_with("biome ")
299 || c.starts_with("stylelint")
300 },
301 |c, output| eslint::compress(c, output),
302 ),
303 (
304 |c| c.starts_with("prettier") || c.starts_with("npx prettier"),
305 |_c, output| prettier::compress(output),
306 ),
307 (
308 |c| c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint"),
309 |c, output| golang::compress(c, output),
310 ),
311 (
312 |c| {
313 c.starts_with("playwright")
314 || c.starts_with("npx playwright")
315 || c.starts_with("cypress")
316 || c.starts_with("npx cypress")
317 },
318 |c, output| playwright::compress(c, output),
319 ),
320 (
321 |c| c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest"),
322 |_c, output| test::compress(output),
323 ),
324 (
325 |c| {
326 c.starts_with("next ")
327 || c.starts_with("npx next")
328 || c.starts_with("vite ")
329 || c.starts_with("npx vite")
330 || c.starts_with("vp ")
331 || c.starts_with("vite-plus ")
332 },
333 |c, output| next_build::compress(c, output),
334 ),
335 (
336 |c| c.starts_with("tsc") || c.contains("typescript"),
337 |_c, output| typescript::compress(output),
338 ),
339 (
340 |c| {
341 c.starts_with("rubocop")
342 || c.starts_with("bundle ")
343 || c.starts_with("rake ")
344 || c.starts_with("rails test")
345 || c.starts_with("rspec")
346 },
347 |c, output| ruby::compress(c, output),
348 ),
349 (|c| is_grep_family(c), |_c, output| grep::compress(output)),
350 (
351 |c| c.starts_with("find "),
352 |_c, output| find::compress(output),
353 ),
354 (
355 |c| c.starts_with("fd ") || c.starts_with("fdfind "),
356 |_c, output| fd::compress(output),
357 ),
358 (
359 |c| c.starts_with("ls ") || c == "ls",
360 |_c, output| ls::compress(output),
361 ),
362 (
363 |c| c.starts_with("curl "),
364 |c, output| curl::compress_with_cmd(c, output),
365 ),
366 (
367 |c| c.starts_with("wget "),
368 |_c, output| wget::compress(output),
369 ),
370 (
371 |c| c == "env" || c.starts_with("env ") || c.starts_with("printenv"),
372 |_c, output| env_filter::compress(output),
373 ),
374 (
375 |c| c.starts_with("dotnet "),
376 |c, output| dotnet::compress(c, output),
377 ),
378 (
379 |c| {
380 c.starts_with("flutter ")
381 || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
382 },
383 |c, output| flutter::compress(c, output),
384 ),
385 (
386 |c| {
387 c.starts_with("poetry ")
388 || c.starts_with("uv ")
389 || c.starts_with("conda ")
390 || c.starts_with("mamba ")
391 || c.starts_with("pipx ")
392 },
393 |c, output| poetry::compress(c, output),
394 ),
395 (
396 |c| c.starts_with("aws "),
397 |c, output| aws::compress(c, output),
398 ),
399 (
400 |c| c.starts_with("psql ") || c.starts_with("pg_"),
401 |c, output| psql::compress(c, output),
402 ),
403 (
404 |c| c.starts_with("mysql ") || c.starts_with("mariadb "),
405 |c, output| mysql::compress(c, output),
406 ),
407 (
408 |c| c.starts_with("prisma ") || c.starts_with("npx prisma"),
409 |c, output| prisma::compress(c, output),
410 ),
411 (
412 |c| c.starts_with("swift "),
413 |c, output| swift::compress(c, output),
414 ),
415 (
416 |c| c.starts_with("zig "),
417 |c, output| zig::compress(c, output),
418 ),
419 (
420 |c| c.starts_with("cmake ") || c.starts_with("ctest"),
421 |c, output| cmake::compress(c, output),
422 ),
423 (
424 |c| c.starts_with("ninja"),
425 |c, output| ninja::compress(c, output),
426 ),
427 (
428 |c| c.starts_with("ansible") || c.starts_with("ansible-playbook"),
429 |c, output| ansible::compress(c, output),
430 ),
431 (
432 |c| c.starts_with("composer "),
433 |c, output| composer::compress(c, output),
434 ),
435 (
436 |c| c.starts_with("php artisan") || c.starts_with("artisan "),
437 |c, output| artisan::compress(c, output),
438 ),
439 (
440 |c| c.starts_with("./vendor/bin/pest") || c.starts_with("pest "),
441 |_c, output| artisan::compress("php artisan test", output),
442 ),
443 (
444 |c| c.starts_with("mix ") || c.starts_with("iex "),
445 |c, output| mix::compress(c, output),
446 ),
447 (
448 |c| c.starts_with("bazel ") || c.starts_with("blaze "),
449 |c, output| bazel::compress(c, output),
450 ),
451 (
452 |c| c.starts_with("systemctl ") || c.starts_with("journalctl"),
453 |c, output| systemd::compress(c, output),
454 ),
455 (
456 |c| c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest"),
457 |_c, output| test::compress(output),
458 ),
459 (
460 |c| c.starts_with("mocha") || c.starts_with("npx mocha"),
461 |_c, output| test::compress(output),
462 ),
463 (
464 |c| c.starts_with("tofu "),
465 |c, output| terraform::compress(c, output),
466 ),
467 (
468 |c| c.starts_with("ps ") || c == "ps",
469 |_c, output| sysinfo::compress_ps(output),
470 ),
471 (
472 |c| c.starts_with("df ") || c == "df",
473 |_c, output| sysinfo::compress_df(output),
474 ),
475 (
476 |c| c.starts_with("du ") || c == "du",
477 |_c, output| sysinfo::compress_du(output),
478 ),
479 (
480 |c| c.starts_with("ping "),
481 |_c, output| sysinfo::compress_ping(output),
482 ),
483 (
484 |c| c.starts_with("jq ") || c == "jq",
485 |_c, output| json_schema::compress(output),
486 ),
487 (
488 |c| c.starts_with("hadolint"),
489 |c, output| eslint::compress(c, output),
490 ),
491 (
492 |c| c.starts_with("yamllint") || c.starts_with("npx yamllint"),
493 |c, output| eslint::compress(c, output),
494 ),
495 (
496 |c| c.starts_with("markdownlint") || c.starts_with("npx markdownlint"),
497 |c, output| eslint::compress(c, output),
498 ),
499 (
500 |c| c.starts_with("oxlint") || c.starts_with("npx oxlint"),
501 |c, output| eslint::compress(c, output),
502 ),
503 (
504 |c| c.starts_with("pyright") || c.starts_with("basedpyright"),
505 |c, output| mypy::compress(c, output),
506 ),
507 (
508 |c| c.starts_with("turbo ") || c.starts_with("npx turbo"),
509 |c, output| npm::compress(c, output),
510 ),
511 (
512 |c| c.starts_with("nx ") || c.starts_with("npx nx"),
513 |c, output| npm::compress(c, output),
514 ),
515 (
516 |c| c.starts_with("clang++ ") || c.starts_with("clang "),
517 |c, output| clang::compress(c, output),
518 ),
519 (
520 |c| {
521 c.starts_with("gcc ")
522 || c.starts_with("g++ ")
523 || c.starts_with("cc ")
524 || c.starts_with("c++ ")
525 },
526 |c, output| cmake::compress(c, output),
527 ),
528 (
530 |c| c == "dbt" || c.starts_with("dbt "),
531 |c, output| dbt::compress(c, output),
532 ),
533 (
534 |c| c == "alembic" || c.starts_with("alembic "),
535 |c, output| alembic::compress(c, output),
536 ),
537 (
538 |c| c == "flyway" || c.starts_with("flyway "),
539 |c, output| flyway::compress(c, output),
540 ),
541 (
542 |c| c.starts_with("spark-submit") || c.starts_with("spark-sql") || c.starts_with("pyspark"),
543 |c, output| spark::compress(c, output),
544 ),
545 (
547 |c| c == "ollama" || c.starts_with("ollama "),
548 |c, output| ollama::compress(c, output),
549 ),
550 (
551 |c| c.starts_with("mlflow "),
552 |c, output| mlflow::compress(c, output),
553 ),
554 (
556 |c| c.starts_with("semgrep "),
557 |c, output| semgrep::compress(c, output),
558 ),
559 (
560 |c| c.starts_with("trivy "),
561 |c, output| trivy::compress(c, output),
562 ),
563 (
564 |c| c.starts_with("grype "),
565 |c, output| grype::compress(c, output),
566 ),
567 (
568 |c| c.starts_with("syft "),
569 |c, output| syft::compress(c, output),
570 ),
571 (
572 |c| c.starts_with("cosign "),
573 |c, output| cosign::compress(c, output),
574 ),
575 (
576 |c| c.starts_with("swiftlint"),
577 |c, output| swiftlint::compress(c, output),
578 ),
579 (
581 |c| c == "jj" || c.starts_with("jj "),
582 |c, output| jj::compress(c, output),
583 ),
584 (
585 |c| c == "mise" || c.starts_with("mise "),
586 |c, output| mise::compress(c, output),
587 ),
588 (
589 |c| c == "buf" || c.starts_with("buf "),
590 |c, output| buf::compress(c, output),
591 ),
592 (
593 |c| c.starts_with("gem "),
594 |c, output| gem::compress(c, output),
595 ),
596 (
598 |c| c == "pulumi" || c.starts_with("pulumi "),
599 |c, output| pulumi::compress(c, output),
600 ),
601 (
602 |c| c.starts_with("linkerd "),
603 |c, output| linkerd::compress(c, output),
604 ),
605 (
606 |c| c.starts_with("argocd "),
607 |c, output| argocd::compress(c, output),
608 ),
609 (
610 |c| {
611 c == "vercel"
612 || c.starts_with("vercel ")
613 || c == "fly"
614 || c.starts_with("fly ")
615 || c.starts_with("flyctl ")
616 || c.starts_with("wrangler ")
617 || c.starts_with("skaffold ")
618 || c.starts_with("supabase ")
619 },
620 |c, output| deploy::compress(c, output),
621 ),
622];
623
624fn is_grep_family(cmd: &str) -> bool {
631 let first = cmd.split_whitespace().next().unwrap_or("");
632 let base = first.rsplit('/').next().unwrap_or(first);
633 matches!(
634 base,
635 "grep" | "egrep" | "fgrep" | "rg" | "ag" | "ack" | "ugrep" | "sift"
636 )
637}
638
639pub fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
640 let cl = cmd.to_ascii_lowercase();
641 let c = cl.as_str();
642
643 PATTERNS
644 .iter()
645 .find(|(matches, _)| matches(c))
646 .and_then(|(_, handle)| handle(c, output))
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn routes_git_commands() {
655 let output = "On branch main\nnothing to commit";
656 assert!(compress_output("git status", output).is_some());
657 }
658
659 #[test]
660 fn vcs_path_never_inflates_tokens() {
661 let output = " Abu a\nAa aa_A00A\n\n-";
667 if let Some(compressed) = compress_output("git status", output) {
668 assert!(
669 count_tokens(&compressed) <= count_tokens(output),
670 "VCS compress inflated: {} > {}",
671 count_tokens(&compressed),
672 count_tokens(output),
673 );
674 }
675 }
676
677 #[test]
678 fn routes_cargo_commands() {
679 let output = " Compiling lean-ctx v2.1.1\n Finished `release` profile [optimized] target(s) in 30.5s";
680 assert!(compress_output("cargo build --release", output).is_some());
681 }
682
683 #[test]
684 fn routes_npm_commands() {
685 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";
686 assert!(compress_output("npm install", output).is_some());
687 }
688
689 #[test]
690 fn routes_docker_commands() {
691 let output = "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES";
692 assert!(compress_output("docker ps", output).is_none());
695 let build_output =
696 "Step 1/5 : FROM node:18\n ---> abc123\nStep 2/5 : COPY . .\nSuccessfully built def456";
697 assert!(compress_output("docker build .", build_output).is_some());
698 }
699
700 #[test]
701 fn routes_mypy_commands() {
702 let output = "src/main.py:10: error: Missing return [return]\nFound 1 error in 1 file (checked 3 source files)";
703 assert!(compress_output("mypy .", output).is_some());
704 assert!(compress_output("python -m mypy src/", output).is_some());
705 }
706
707 #[test]
708 fn routes_pytest_commands() {
709 let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
710 assert!(compress_output("pytest", output).is_some());
711 assert!(compress_output("python -m pytest tests/", output).is_some());
712 }
713
714 #[test]
715 fn routes_data_domain() {
716 let dbt =
717 "20:14:02 Found 12 models\n20:14:20 Done. PASS=11 WARN=0 ERROR=1 SKIP=0 TOTAL=12";
718 assert!(
719 compress_output("dbt run", dbt).is_some(),
720 "dbt routed+compressible"
721 );
722 let alembic = "INFO [alembic.runtime.migration] Context impl PostgresqlImpl.\nINFO [alembic.runtime.migration] Running upgrade -> a1b2c3, create users table\nINFO [alembic.runtime.migration] Running upgrade a1b2c3 -> d4e5f6, add email index";
723 assert!(compress_output("alembic upgrade head", alembic).is_some());
724 let flyway = "Flyway Community Edition 9.22.0 by Redgate\nDatabase: jdbc:postgresql://localhost/db\nMigrating schema \"public\" to version \"5 - add orders\"\nSuccessfully applied 1 migration to schema \"public\", now at version v5";
725 assert!(compress_output("flyway migrate", flyway).is_some());
726 let spark = "23/01/01 12:00:00 INFO SparkContext: Running Spark version 3.4.0\n23/01/01 12:00:01 INFO ResourceUtils: none configured\n23/01/01 12:00:10 INFO DAGScheduler: Job 0 finished: collect, took 5.1 s\n23/01/01 12:00:15 ERROR Executor: Exception in task 0.0";
727 assert!(compress_output("spark-submit app.py", spark).is_some());
728 }
729
730 #[test]
731 fn routes_ai_domain() {
732 let ollama = "NAME ID SIZE MODIFIED\nllama3.2:latest a80c4f17acd5 2.0 GB 3 days ago\nqwen2.5-coder:7b 2b0496514337 4.7 GB 2 weeks ago";
733 assert!(compress_output("ollama list", ollama).is_some());
734 let mlflow = "2024/01/01 12:00:01 INFO mlflow.projects.backend.local: === Running command 'python train.py' ===\nCollecting numpy==1.26.0\nDownloading numpy-1.26.0.whl (18.2 MB)\n2024/01/01 12:00:30 INFO mlflow.projects: === Run (ID 'abc123def456') succeeded ===";
735 assert!(compress_output("mlflow run .", mlflow).is_some());
736 }
737
738 #[test]
739 fn routes_security_domain() {
740 let trivy = "2024-01-01T12:00:00.000Z\tINFO\tscanning\nnginx:latest (debian 12.1)\n=====\nTotal: 45 (LOW: 20, HIGH: 8, CRITICAL: 2)";
741 assert!(compress_output("trivy image nginx", trivy).is_some());
742 let grype = "NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY\nlibssl1.1 1.1.1n 1.1.1w deb CVE-2023-1234 Critical\nzlib1g 1.2.11 1.2.13 deb CVE-2022-5678 High";
743 assert!(compress_output("grype nginx", grype).is_some());
744 let syft = "NAME VERSION TYPE\nadduser 3.118 deb\napt 2.6.1 deb\nlodash 4.17.21 npm";
745 assert!(compress_output("syft nginx", syft).is_some());
746 let semgrep = "Scanning 120 files.\n src/app.py\n python.security.dangerous-subprocess-use\n Detected subprocess.\n 42┆ subprocess.call(x)\nRan 450 rules on 120 files: 1 findings.";
747 assert!(compress_output("semgrep scan", semgrep).is_some());
748 let swiftlint = "Linting Swift files in current working directory\nLinting 'A.swift' (1/3)\nLinting 'B.swift' (2/3)\nLinting 'C.swift' (3/3)\n/path/A.swift:10:5: warning: Line Length Violation: Line should be 120 chars or less (line_length)\n/path/A.swift:22:1: warning: Trailing Whitespace Violation: no trailing whitespace (trailing_whitespace)\n/path/B.swift:5:1: error: Force Cast Violation: avoid force casts (force_cast)\n/path/C.swift:8:3: warning: Todo Violation: resolve TODOs (todo)\nDone linting! Found 4 violations, 1 serious in 3 files.";
749 assert!(compress_output("swiftlint", swiftlint).is_some());
750 }
751
752 #[test]
753 fn routes_vcs_toolchain_domain() {
754 let jj = "@ qpvuntsm user@host.com 2024-01-01 12:00:00 1234abcd\n│ add feature x\n○ zzzzmmmm user@host.com 2024-01-01 11:00:00 main 5678efab\n│ initial commit\n~";
755 assert!(compress_output("jj log", jj).is_some());
756 let mise = "node 20.10.0 ~/.config/mise/config.toml\npython 3.12.0 ~/.tool-versions\nrust 1.75.0 ~/.config/mise/config.toml";
757 assert!(compress_output("mise ls", mise).is_some());
758 let buf_lines: Vec<String> = (0..30)
759 .map(|i| format!("proto/f{i}.proto:{i}:1:Field name should be lower_snake_case here."))
760 .collect();
761 let buf = buf_lines.join("\n");
762 assert!(compress_output("buf lint", &buf).is_some());
763 let gem = "Fetching rails-7.1.0.gem\nSuccessfully installed activesupport-7.1.0\nSuccessfully installed rails-7.1.0\nParsing documentation for rails-7.1.0\nInstalling ri documentation for rails-7.1.0\nDone installing documentation for rails after 3 seconds\n2 gems installed";
764 assert!(compress_output("gem install rails", gem).is_some());
765 let uv = "Resolved 42 packages in 120ms\nDownloading numpy (18.2MiB)\n 100%|████████| 18.2M/18.2M [00:01<00:00, 15.3MiB/s]\nDownloading pandas (12.1MiB)\n 100%|████████| 12.1M/12.1M [00:00<00:00, 14.1MiB/s]\nPrepared 5 packages in 1.2s\nInstalled 5 packages in 30ms\n + numpy==1.26.0\n + pandas==2.1.0";
766 assert!(compress_output("uv add pandas", uv).is_some());
767 }
768
769 #[test]
770 fn routes_edge_infra_domain() {
771 let pulumi = "Updating (dev):\n Type Name Status\n + pulumi:pulumi:Stack proj created\n + aws:s3:Bucket b1 created\n + aws:s3:Bucket b2 created\n + aws:s3:Bucket b3 created\n + aws:lambda:Function fn created\n\nOutputs:\n url: \"https://x.example.com\"\n\nResources:\n + 5 created\n 10 unchanged\n\nDuration: 35s";
772 assert!(compress_output("pulumi up", pulumi).is_some());
773 let linkerd = "kubernetes-api\n--------------\n√ can initialize the client\n√ can query the Kubernetes API\n√ is running the minimum kubectl version\n\nlinkerd-existence\n-----------------\n√ 'linkerd-config' config map exists\n× control plane pods are ready\n some pods are not ready\n\nStatus check results are ×";
774 assert!(compress_output("linkerd check", linkerd).is_some());
775 let argocd = "Name: argocd/myapp\nProject: default\nSync Status: Synced\nHealth Status: Healthy\n\nGROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE\n Service ns s1 Synced Healthy\n Service ns s2 Synced Healthy\n Service ns s3 Synced Healthy\napps Deployment ns d1 OutOfSync Progressing";
776 assert!(compress_output("argocd app get myapp", argocd).is_some());
777 let vercel = "Vercel CLI 33.0.0\nInstalling dependencies...\nadded 420 packages in 12s\nBuilding...\nCompiling pages\nCollecting page data\nGenerating static pages\nProduction: https://my-app.vercel.app [45s]";
778 assert!(compress_output("vercel deploy --prod", vercel).is_some());
779 let wrangler = "wrangler 3.0.0\n-------------------\nyour worker has access to the following bindings:\n- KV Namespaces:\n - CACHE: abc123\nTotal Upload: 1.2 MiB / gzip: 0.4 MiB\nUploaded my-worker (3.5 sec)\nPublished my-worker (1.2 sec)\n https://my-worker.example.workers.dev";
780 assert!(compress_output("wrangler deploy", wrangler).is_some());
781 }
782
783 #[test]
784 fn unknown_command_returns_none() {
785 assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
786 }
787
788 #[test]
789 fn case_insensitive_routing() {
790 let output = "On branch main\nnothing to commit";
791 assert!(compress_output("Git Status", output).is_some());
792 assert!(compress_output("GIT STATUS", output).is_some());
793 }
794
795 #[test]
796 fn routes_vp_and_vite_plus() {
797 let output = " VITE v5.0.0 ready in 200 ms\n\n -> Local: http://localhost:5173/\n -> Network: http://192.168.1.2:5173/";
798 assert!(compress_output("vp build", output).is_some());
799 assert!(compress_output("vite-plus build", output).is_some());
800 }
801
802 #[test]
803 fn routes_bunx_commands() {
804 let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
805 let result = compress_output("bunx test", output);
806 assert!(
807 result.is_some(),
808 "bunx should compress when output is large enough"
809 );
810 assert!(result.unwrap().contains("bun test: 1 passed"));
811 }
812
813 #[test]
814 fn routes_deno_task() {
815 let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
816 assert!(try_specific_pattern("deno task dev", output).is_some());
817 }
818
819 #[test]
820 fn routes_jest_commands() {
821 let output = "PASS tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests: 5 passed, 5 total\nTime: 2.5 s";
822 assert!(try_specific_pattern("jest", output).is_some());
823 assert!(try_specific_pattern("npx jest --coverage", output).is_some());
824 }
825
826 #[test]
827 fn routes_mocha_commands() {
828 let output = " 3 passing (50ms)\n 1 failing\n\n 1) Array #indexOf():\n Error: expected -1 to equal 0";
829 assert!(try_specific_pattern("mocha", output).is_some());
830 assert!(try_specific_pattern("npx mocha tests/", output).is_some());
831 }
832
833 #[test]
834 fn routes_tofu_commands() {
835 let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
836 assert!(try_specific_pattern("tofu init", output).is_some());
837 }
838
839 #[test]
840 fn routes_ps_commands() {
841 let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
842 for i in 0..20 {
843 lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
844 }
845 let output = lines.join("\n");
846 assert!(try_specific_pattern("ps aux", &output).is_some());
847 }
848
849 #[test]
850 fn routes_ping_commands() {
851 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";
852 assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
853 }
854
855 #[test]
856 fn routes_jq_to_json_schema() {
857 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}]}";
858 assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
859 }
860
861 #[test]
862 fn routes_linting_tools() {
863 let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
864 assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
865 assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
866 assert!(try_specific_pattern("pyright src/", lint_output).is_some());
867 assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
868 }
869
870 #[test]
871 fn routes_fd_commands() {
872 let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
873 assert!(try_specific_pattern("fd --extension rs", output).is_some());
874 assert!(try_specific_pattern("fdfind .rs", output).is_some());
875 }
876
877 #[test]
878 fn routes_just_commands() {
879 let output = "Available recipes:\n build\n test\n lint\n";
880 assert!(try_specific_pattern("just --list", output).is_some());
881 assert!(try_specific_pattern("just build", output).is_some());
882 }
883
884 #[test]
885 fn routes_ninja_commands() {
886 let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
887 assert!(try_specific_pattern("ninja", output).is_some());
888 assert!(try_specific_pattern("ninja -j4", output).is_some());
889 }
890
891 #[test]
892 fn routes_clang_commands() {
893 let output =
894 "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
895 assert!(try_specific_pattern("clang src/main.c", output).is_some());
896 assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
897 }
898
899 #[test]
900 fn routes_cargo_run() {
901 let output = " Compiling foo v0.1.0\n Finished `dev` profile\nHello, world!";
902 assert!(try_specific_pattern("cargo run", output).is_some());
903 }
904
905 #[test]
906 fn routes_cargo_bench() {
907 let output = " Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
908 assert!(try_specific_pattern("cargo bench", output).is_some());
909 }
910
911 #[test]
912 fn routes_build_tools() {
913 let build_output = " Compiling foo v0.1.0\n Finished release [optimized]";
914 assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
915 assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
916 }
917
918 #[test]
919 fn routes_monorepo_tools() {
920 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";
921 assert!(try_specific_pattern("turbo install", output).is_some());
922 assert!(try_specific_pattern("nx install", output).is_some());
923 }
924
925 #[test]
926 fn gh_api_passthrough_never_compresses() {
927 let huge = "line\n".repeat(5000);
928 assert!(
929 compress_output("gh api repos/owner/repo/actions/jobs/123/logs", &huge).is_none(),
930 "gh api must never be compressed, even for large output"
931 );
932 assert!(compress_output("gh api repos/owner/repo/actions/runs/123/logs", &huge).is_none());
933 }
934
935 #[test]
936 fn gh_log_flags_passthrough() {
937 let huge = "line\n".repeat(5000);
938 assert!(compress_output("gh run view 123 --log-failed", &huge).is_none());
939 assert!(compress_output("gh run view 123 --log", &huge).is_none());
940 }
941
942 #[test]
943 fn gh_structured_commands_still_compress() {
944 let output = "On branch main\nnothing to commit";
945 assert!(try_specific_pattern("gh pr list", output).is_some());
946 assert!(try_specific_pattern("gh run list", output).is_some());
947 }
948}