1pub const PATTERN_ENGINE_VERSION: u32 = 1;
5
6pub mod alembic;
7pub mod ansible;
8pub mod argocd;
9pub mod artisan;
10pub mod aws;
11pub mod bazel;
12pub mod buf;
13pub mod bun;
14pub mod cargo;
15pub mod clang;
16pub mod cmake;
17pub mod composer;
18pub mod cosign;
19pub mod curl;
20pub mod dbt;
21pub mod deno;
22pub mod deploy;
23pub mod deps_cmd;
24pub mod docker;
25pub mod dotnet;
26pub mod env_filter;
27pub mod eslint;
28pub mod fd;
29pub mod find;
30pub mod flutter;
31pub mod flyway;
32pub mod gem;
33pub mod gh;
34pub mod git;
35pub mod glab;
36pub mod golang;
37pub mod grep;
38pub mod grype;
39pub mod helm;
40pub mod jj;
41pub mod json_schema;
42pub mod just;
43pub mod kubectl;
44pub mod linkerd;
45pub mod log_dedup;
46pub mod ls;
47pub mod make;
48pub mod maven;
49pub mod mise;
50pub mod mix;
51pub mod mlflow;
52pub mod mypy;
53pub mod mysql;
54pub mod next_build;
55pub mod ninja;
56pub mod npm;
57pub mod ollama;
58pub mod php;
59pub mod pip;
60pub mod playwright;
61pub mod pnpm;
62pub mod poetry;
63pub mod prettier;
64pub mod prisma;
65pub mod psql;
66pub mod pulumi;
67pub mod pytest;
68pub mod ruby;
69pub mod ruff;
70pub mod semgrep;
71pub mod spark;
72pub mod swift;
73pub mod swiftlint;
74pub mod syft;
75pub mod sysinfo;
76pub mod systemd;
77pub mod terraform;
78pub mod test;
79pub mod trivy;
80pub mod typescript;
81pub mod wget;
82pub mod zig;
83
84use crate::core::tokens::count_tokens;
85
86pub fn compress_output(command: &str, output: &str) -> Option<String> {
87 let policy = crate::shell::output_policy::classify(command, &[]);
92 if policy.is_protected() {
93 return None;
94 }
95
96 let stripped;
97 let clean_output = {
98 let s = crate::core::compressor::strip_ansi(output);
99 if s.len() < output.len() {
100 stripped = s;
101 &stripped
102 } else {
103 output
104 }
105 };
106
107 if let Some(engine) = crate::core::filters::FilterEngine::load()
108 && let Some(filtered) = engine.apply(command, clean_output)
109 {
110 return shorter_only(filtered, output);
111 }
112
113 if has_vcs_owner(command) {
122 return try_specific_pattern(command, clean_output).filter(|c| !c.trim().is_empty());
123 }
124
125 if let Some(compressed) = try_specific_pattern(command, clean_output)
126 && let Some(r) = shorter_only(compressed, output)
127 {
128 return Some(r);
129 }
130
131 if let Some(r) = json_schema::compress(clean_output)
132 && let Some(r) = shorter_only(r, output)
133 {
134 return Some(r);
135 }
136
137 if let Some(r) = log_dedup::compress(clean_output)
138 && let Some(r) = shorter_only(r, output)
139 {
140 return Some(r);
141 }
142
143 if let Some(r) = test::compress(clean_output)
144 && let Some(r) = shorter_only(r, output)
145 {
146 return Some(r);
147 }
148
149 None
150}
151
152pub(crate) fn has_vcs_owner(command: &str) -> bool {
156 let c = command.trim_start().to_ascii_lowercase();
157 c.starts_with("git ")
158 || c.starts_with("jj ")
159 || c.starts_with("gh ")
160 || c.starts_with("glab ")
161 || c.starts_with("hg ")
162}
163
164fn normalize_shell_tokens(text: &str) -> String {
166 text.split_whitespace().collect::<Vec<_>>().join(" ")
167}
168
169fn shorter_only(compressed: String, original: &str) -> Option<String> {
170 let orig_n = normalize_shell_tokens(original);
171 let comp_n = normalize_shell_tokens(&compressed);
172 let ct_c = count_tokens(&comp_n);
173 let ct_o = count_tokens(&orig_n);
174 if ct_c < ct_o || (ct_c == ct_o && comp_n.len() < orig_n.len()) {
175 Some(compressed)
176 } else {
177 None
178 }
179}
180
181pub fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
182 let cl = cmd.to_ascii_lowercase();
183 let c = cl.as_str();
184
185 if c.starts_with("git ") {
186 return git::compress(c, output);
187 }
188 if c.starts_with("gh ") {
189 return gh::compress(c, output);
190 }
191 if c.starts_with("glab ") {
192 return glab::try_glab_pattern(c, output);
193 }
194 if c == "terraform" || c.starts_with("terraform ") {
195 return terraform::compress(c, output);
196 }
197 if c == "make" || c.starts_with("make ") {
198 return make::compress(c, output);
199 }
200 if c == "just" || c.starts_with("just ") {
201 return just::compress(c, output);
202 }
203 if c.starts_with("mvn ")
204 || c.starts_with("./mvnw ")
205 || c.starts_with("mvnw ")
206 || c.starts_with("gradle ")
207 || c.starts_with("./gradlew ")
208 || c.starts_with("gradlew ")
209 {
210 return maven::compress(c, output);
211 }
212 if c.starts_with("kubectl ") || c.starts_with("k ") {
213 return kubectl::compress(c, output);
214 }
215 if c.starts_with("helm ") {
216 return helm::compress(c, output);
217 }
218 if c.starts_with("pnpm ") {
219 return pnpm::compress(c, output);
220 }
221 if c.starts_with("bun ") || c.starts_with("bunx ") {
222 return bun::compress(c, output);
223 }
224 if c.starts_with("deno ") {
225 return deno::compress(c, output);
226 }
227 if c.starts_with("npm ") || c.starts_with("yarn ") {
228 return npm::compress(c, output);
229 }
230 if c.starts_with("cargo ") {
231 return cargo::compress(c, output);
232 }
233 if c.starts_with("docker ") || c.starts_with("docker-compose ") {
234 return docker::compress(c, output);
235 }
236 if c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip") {
237 return pip::compress(c, output);
238 }
239 if c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy ") {
240 return mypy::compress(c, output);
241 }
242 if c.starts_with("pytest") || c.starts_with("python -m pytest") {
243 return pytest::compress(c, output).or_else(|| test::compress(output));
244 }
245 if c.starts_with("ruff ") {
246 return ruff::compress(c, output);
247 }
248 if c.starts_with("eslint")
249 || c.starts_with("npx eslint")
250 || c.starts_with("biome ")
251 || c.starts_with("stylelint")
252 {
253 return eslint::compress(c, output);
254 }
255 if c.starts_with("prettier") || c.starts_with("npx prettier") {
256 return prettier::compress(output);
257 }
258 if c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint") {
259 return golang::compress(c, output);
260 }
261 if c.starts_with("playwright")
262 || c.starts_with("npx playwright")
263 || c.starts_with("cypress")
264 || c.starts_with("npx cypress")
265 {
266 return playwright::compress(c, output);
267 }
268 if c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest") {
269 return test::compress(output);
270 }
271 if c.starts_with("next ")
272 || c.starts_with("npx next")
273 || c.starts_with("vite ")
274 || c.starts_with("npx vite")
275 || c.starts_with("vp ")
276 || c.starts_with("vite-plus ")
277 {
278 return next_build::compress(c, output);
279 }
280 if c.starts_with("tsc") || c.contains("typescript") {
281 return typescript::compress(output);
282 }
283 if c.starts_with("rubocop")
284 || c.starts_with("bundle ")
285 || c.starts_with("rake ")
286 || c.starts_with("rails test")
287 || c.starts_with("rspec")
288 {
289 return ruby::compress(c, output);
290 }
291 if c.starts_with("grep ") || c.starts_with("rg ") {
292 return grep::compress(output);
293 }
294 if c.starts_with("find ") {
295 return find::compress(output);
296 }
297 if c.starts_with("fd ") || c.starts_with("fdfind ") {
298 return fd::compress(output);
299 }
300 if c.starts_with("ls ") || c == "ls" {
301 return ls::compress(output);
302 }
303 if c.starts_with("curl ") {
304 return curl::compress_with_cmd(c, output);
305 }
306 if c.starts_with("wget ") {
307 return wget::compress(output);
308 }
309 if c == "env" || c.starts_with("env ") || c.starts_with("printenv") {
310 return env_filter::compress(output);
311 }
312 if c.starts_with("dotnet ") {
313 return dotnet::compress(c, output);
314 }
315 if c.starts_with("flutter ")
316 || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
317 {
318 return flutter::compress(c, output);
319 }
320 if c.starts_with("poetry ")
321 || c.starts_with("uv ")
322 || c.starts_with("conda ")
323 || c.starts_with("mamba ")
324 || c.starts_with("pipx ")
325 {
326 return poetry::compress(c, output);
327 }
328 if c.starts_with("aws ") {
329 return aws::compress(c, output);
330 }
331 if c.starts_with("psql ") || c.starts_with("pg_") {
332 return psql::compress(c, output);
333 }
334 if c.starts_with("mysql ") || c.starts_with("mariadb ") {
335 return mysql::compress(c, output);
336 }
337 if c.starts_with("prisma ") || c.starts_with("npx prisma") {
338 return prisma::compress(c, output);
339 }
340 if c.starts_with("swift ") {
341 return swift::compress(c, output);
342 }
343 if c.starts_with("zig ") {
344 return zig::compress(c, output);
345 }
346 if c.starts_with("cmake ") || c.starts_with("ctest") {
347 return cmake::compress(c, output);
348 }
349 if c.starts_with("ninja") {
350 return ninja::compress(c, output);
351 }
352 if c.starts_with("ansible") || c.starts_with("ansible-playbook") {
353 return ansible::compress(c, output);
354 }
355 if c.starts_with("composer ") {
356 return composer::compress(c, output);
357 }
358 if c.starts_with("php artisan") || c.starts_with("artisan ") {
359 return artisan::compress(c, output);
360 }
361 if c.starts_with("./vendor/bin/pest") || c.starts_with("pest ") {
362 return artisan::compress("php artisan test", output);
363 }
364 if c.starts_with("mix ") || c.starts_with("iex ") {
365 return mix::compress(c, output);
366 }
367 if c.starts_with("bazel ") || c.starts_with("blaze ") {
368 return bazel::compress(c, output);
369 }
370 if c.starts_with("systemctl ") || c.starts_with("journalctl") {
371 return systemd::compress(c, output);
372 }
373 if c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest") {
374 return test::compress(output);
375 }
376 if c.starts_with("mocha") || c.starts_with("npx mocha") {
377 return test::compress(output);
378 }
379 if c.starts_with("tofu ") {
380 return terraform::compress(c, output);
381 }
382 if c.starts_with("ps ") || c == "ps" {
383 return sysinfo::compress_ps(output);
384 }
385 if c.starts_with("df ") || c == "df" {
386 return sysinfo::compress_df(output);
387 }
388 if c.starts_with("du ") || c == "du" {
389 return sysinfo::compress_du(output);
390 }
391 if c.starts_with("ping ") {
392 return sysinfo::compress_ping(output);
393 }
394 if c.starts_with("jq ") || c == "jq" {
395 return json_schema::compress(output);
396 }
397 if c.starts_with("hadolint") {
398 return eslint::compress(c, output);
399 }
400 if c.starts_with("yamllint") || c.starts_with("npx yamllint") {
401 return eslint::compress(c, output);
402 }
403 if c.starts_with("markdownlint") || c.starts_with("npx markdownlint") {
404 return eslint::compress(c, output);
405 }
406 if c.starts_with("oxlint") || c.starts_with("npx oxlint") {
407 return eslint::compress(c, output);
408 }
409 if c.starts_with("pyright") || c.starts_with("basedpyright") {
410 return mypy::compress(c, output);
411 }
412 if c.starts_with("turbo ") || c.starts_with("npx turbo") {
413 return npm::compress(c, output);
414 }
415 if c.starts_with("nx ") || c.starts_with("npx nx") {
416 return npm::compress(c, output);
417 }
418 if c.starts_with("clang++ ") || c.starts_with("clang ") {
419 return clang::compress(c, output);
420 }
421 if c.starts_with("gcc ")
422 || c.starts_with("g++ ")
423 || c.starts_with("cc ")
424 || c.starts_with("c++ ")
425 {
426 return cmake::compress(c, output);
427 }
428
429 if c == "dbt" || c.starts_with("dbt ") {
431 return dbt::compress(c, output);
432 }
433 if c == "alembic" || c.starts_with("alembic ") {
434 return alembic::compress(c, output);
435 }
436 if c == "flyway" || c.starts_with("flyway ") {
437 return flyway::compress(c, output);
438 }
439 if c.starts_with("spark-submit") || c.starts_with("spark-sql") || c.starts_with("pyspark") {
440 return spark::compress(c, output);
441 }
442
443 if c == "ollama" || c.starts_with("ollama ") {
445 return ollama::compress(c, output);
446 }
447 if c.starts_with("mlflow ") {
448 return mlflow::compress(c, output);
449 }
450
451 if c.starts_with("semgrep ") {
453 return semgrep::compress(c, output);
454 }
455 if c.starts_with("trivy ") {
456 return trivy::compress(c, output);
457 }
458 if c.starts_with("grype ") {
459 return grype::compress(c, output);
460 }
461 if c.starts_with("syft ") {
462 return syft::compress(c, output);
463 }
464 if c.starts_with("cosign ") {
465 return cosign::compress(c, output);
466 }
467 if c.starts_with("swiftlint") {
468 return swiftlint::compress(c, output);
469 }
470
471 if c == "jj" || c.starts_with("jj ") {
473 return jj::compress(c, output);
474 }
475 if c == "mise" || c.starts_with("mise ") {
476 return mise::compress(c, output);
477 }
478 if c == "buf" || c.starts_with("buf ") {
479 return buf::compress(c, output);
480 }
481 if c.starts_with("gem ") {
482 return gem::compress(c, output);
483 }
484
485 if c == "pulumi" || c.starts_with("pulumi ") {
487 return pulumi::compress(c, output);
488 }
489 if c.starts_with("linkerd ") {
490 return linkerd::compress(c, output);
491 }
492 if c.starts_with("argocd ") {
493 return argocd::compress(c, output);
494 }
495 if c == "vercel"
496 || c.starts_with("vercel ")
497 || c == "fly"
498 || c.starts_with("fly ")
499 || c.starts_with("flyctl ")
500 || c.starts_with("wrangler ")
501 || c.starts_with("skaffold ")
502 || c.starts_with("supabase ")
503 {
504 return deploy::compress(c, output);
505 }
506
507 None
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn routes_git_commands() {
516 let output = "On branch main\nnothing to commit";
517 assert!(compress_output("git status", output).is_some());
518 }
519
520 #[test]
521 fn routes_cargo_commands() {
522 let output = " Compiling lean-ctx v2.1.1\n Finished `release` profile [optimized] target(s) in 30.5s";
523 assert!(compress_output("cargo build --release", output).is_some());
524 }
525
526 #[test]
527 fn routes_npm_commands() {
528 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";
529 assert!(compress_output("npm install", output).is_some());
530 }
531
532 #[test]
533 fn routes_docker_commands() {
534 let output = "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES";
535 assert!(compress_output("docker ps", output).is_none());
538 let build_output =
539 "Step 1/5 : FROM node:18\n ---> abc123\nStep 2/5 : COPY . .\nSuccessfully built def456";
540 assert!(compress_output("docker build .", build_output).is_some());
541 }
542
543 #[test]
544 fn routes_mypy_commands() {
545 let output = "src/main.py:10: error: Missing return [return]\nFound 1 error in 1 file (checked 3 source files)";
546 assert!(compress_output("mypy .", output).is_some());
547 assert!(compress_output("python -m mypy src/", output).is_some());
548 }
549
550 #[test]
551 fn routes_pytest_commands() {
552 let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
553 assert!(compress_output("pytest", output).is_some());
554 assert!(compress_output("python -m pytest tests/", output).is_some());
555 }
556
557 #[test]
558 fn routes_data_domain() {
559 let dbt =
560 "20:14:02 Found 12 models\n20:14:20 Done. PASS=11 WARN=0 ERROR=1 SKIP=0 TOTAL=12";
561 assert!(
562 compress_output("dbt run", dbt).is_some(),
563 "dbt routed+compressible"
564 );
565 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";
566 assert!(compress_output("alembic upgrade head", alembic).is_some());
567 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";
568 assert!(compress_output("flyway migrate", flyway).is_some());
569 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";
570 assert!(compress_output("spark-submit app.py", spark).is_some());
571 }
572
573 #[test]
574 fn routes_ai_domain() {
575 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";
576 assert!(compress_output("ollama list", ollama).is_some());
577 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 ===";
578 assert!(compress_output("mlflow run .", mlflow).is_some());
579 }
580
581 #[test]
582 fn routes_security_domain() {
583 let trivy = "2024-01-01T12:00:00.000Z\tINFO\tscanning\nnginx:latest (debian 12.1)\n=====\nTotal: 45 (LOW: 20, HIGH: 8, CRITICAL: 2)";
584 assert!(compress_output("trivy image nginx", trivy).is_some());
585 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";
586 assert!(compress_output("grype nginx", grype).is_some());
587 let syft = "NAME VERSION TYPE\nadduser 3.118 deb\napt 2.6.1 deb\nlodash 4.17.21 npm";
588 assert!(compress_output("syft nginx", syft).is_some());
589 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.";
590 assert!(compress_output("semgrep scan", semgrep).is_some());
591 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.";
592 assert!(compress_output("swiftlint", swiftlint).is_some());
593 }
594
595 #[test]
596 fn routes_vcs_toolchain_domain() {
597 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~";
598 assert!(compress_output("jj log", jj).is_some());
599 let mise = "node 20.10.0 ~/.config/mise/config.toml\npython 3.12.0 ~/.tool-versions\nrust 1.75.0 ~/.config/mise/config.toml";
600 assert!(compress_output("mise ls", mise).is_some());
601 let buf_lines: Vec<String> = (0..30)
602 .map(|i| format!("proto/f{i}.proto:{i}:1:Field name should be lower_snake_case here."))
603 .collect();
604 let buf = buf_lines.join("\n");
605 assert!(compress_output("buf lint", &buf).is_some());
606 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";
607 assert!(compress_output("gem install rails", gem).is_some());
608 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";
609 assert!(compress_output("uv add pandas", uv).is_some());
610 }
611
612 #[test]
613 fn routes_edge_infra_domain() {
614 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";
615 assert!(compress_output("pulumi up", pulumi).is_some());
616 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 ×";
617 assert!(compress_output("linkerd check", linkerd).is_some());
618 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";
619 assert!(compress_output("argocd app get myapp", argocd).is_some());
620 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]";
621 assert!(compress_output("vercel deploy --prod", vercel).is_some());
622 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";
623 assert!(compress_output("wrangler deploy", wrangler).is_some());
624 }
625
626 #[test]
627 fn unknown_command_returns_none() {
628 assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
629 }
630
631 #[test]
632 fn case_insensitive_routing() {
633 let output = "On branch main\nnothing to commit";
634 assert!(compress_output("Git Status", output).is_some());
635 assert!(compress_output("GIT STATUS", output).is_some());
636 }
637
638 #[test]
639 fn routes_vp_and_vite_plus() {
640 let output = " VITE v5.0.0 ready in 200 ms\n\n -> Local: http://localhost:5173/\n -> Network: http://192.168.1.2:5173/";
641 assert!(compress_output("vp build", output).is_some());
642 assert!(compress_output("vite-plus build", output).is_some());
643 }
644
645 #[test]
646 fn routes_bunx_commands() {
647 let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
648 let result = compress_output("bunx test", output);
649 assert!(
650 result.is_some(),
651 "bunx should compress when output is large enough"
652 );
653 assert!(result.unwrap().contains("bun test: 1 passed"));
654 }
655
656 #[test]
657 fn routes_deno_task() {
658 let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
659 assert!(try_specific_pattern("deno task dev", output).is_some());
660 }
661
662 #[test]
663 fn routes_jest_commands() {
664 let output = "PASS tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests: 5 passed, 5 total\nTime: 2.5 s";
665 assert!(try_specific_pattern("jest", output).is_some());
666 assert!(try_specific_pattern("npx jest --coverage", output).is_some());
667 }
668
669 #[test]
670 fn routes_mocha_commands() {
671 let output = " 3 passing (50ms)\n 1 failing\n\n 1) Array #indexOf():\n Error: expected -1 to equal 0";
672 assert!(try_specific_pattern("mocha", output).is_some());
673 assert!(try_specific_pattern("npx mocha tests/", output).is_some());
674 }
675
676 #[test]
677 fn routes_tofu_commands() {
678 let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
679 assert!(try_specific_pattern("tofu init", output).is_some());
680 }
681
682 #[test]
683 fn routes_ps_commands() {
684 let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
685 for i in 0..20 {
686 lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
687 }
688 let output = lines.join("\n");
689 assert!(try_specific_pattern("ps aux", &output).is_some());
690 }
691
692 #[test]
693 fn routes_ping_commands() {
694 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";
695 assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
696 }
697
698 #[test]
699 fn routes_jq_to_json_schema() {
700 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}]}";
701 assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
702 }
703
704 #[test]
705 fn routes_linting_tools() {
706 let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
707 assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
708 assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
709 assert!(try_specific_pattern("pyright src/", lint_output).is_some());
710 assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
711 }
712
713 #[test]
714 fn routes_fd_commands() {
715 let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
716 assert!(try_specific_pattern("fd --extension rs", output).is_some());
717 assert!(try_specific_pattern("fdfind .rs", output).is_some());
718 }
719
720 #[test]
721 fn routes_just_commands() {
722 let output = "Available recipes:\n build\n test\n lint\n";
723 assert!(try_specific_pattern("just --list", output).is_some());
724 assert!(try_specific_pattern("just build", output).is_some());
725 }
726
727 #[test]
728 fn routes_ninja_commands() {
729 let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
730 assert!(try_specific_pattern("ninja", output).is_some());
731 assert!(try_specific_pattern("ninja -j4", output).is_some());
732 }
733
734 #[test]
735 fn routes_clang_commands() {
736 let output =
737 "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
738 assert!(try_specific_pattern("clang src/main.c", output).is_some());
739 assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
740 }
741
742 #[test]
743 fn routes_cargo_run() {
744 let output = " Compiling foo v0.1.0\n Finished `dev` profile\nHello, world!";
745 assert!(try_specific_pattern("cargo run", output).is_some());
746 }
747
748 #[test]
749 fn routes_cargo_bench() {
750 let output = " Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
751 assert!(try_specific_pattern("cargo bench", output).is_some());
752 }
753
754 #[test]
755 fn routes_build_tools() {
756 let build_output = " Compiling foo v0.1.0\n Finished release [optimized]";
757 assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
758 assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
759 }
760
761 #[test]
762 fn routes_monorepo_tools() {
763 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";
764 assert!(try_specific_pattern("turbo install", output).is_some());
765 assert!(try_specific_pattern("nx install", output).is_some());
766 }
767
768 #[test]
769 fn gh_api_passthrough_never_compresses() {
770 let huge = "line\n".repeat(5000);
771 assert!(
772 compress_output("gh api repos/owner/repo/actions/jobs/123/logs", &huge).is_none(),
773 "gh api must never be compressed, even for large output"
774 );
775 assert!(compress_output("gh api repos/owner/repo/actions/runs/123/logs", &huge).is_none());
776 }
777
778 #[test]
779 fn gh_log_flags_passthrough() {
780 let huge = "line\n".repeat(5000);
781 assert!(compress_output("gh run view 123 --log-failed", &huge).is_none());
782 assert!(compress_output("gh run view 123 --log", &huge).is_none());
783 }
784
785 #[test]
786 fn gh_structured_commands_still_compress() {
787 let output = "On branch main\nnothing to commit";
788 assert!(try_specific_pattern("gh pr list", output).is_some());
789 assert!(try_specific_pattern("gh run list", output).is_some());
790 }
791}