Skip to main content

lean_ctx/core/patterns/
mod.rs

1pub mod ansible;
2pub mod artisan;
3pub mod aws;
4pub mod bazel;
5pub mod bun;
6pub mod cargo;
7pub mod cmake;
8pub mod composer;
9pub mod curl;
10pub mod deno;
11pub mod deps_cmd;
12pub mod docker;
13pub mod dotnet;
14pub mod env_filter;
15pub mod eslint;
16pub mod find;
17pub mod flutter;
18pub mod gh;
19pub mod git;
20pub mod golang;
21pub mod grep;
22pub mod helm;
23pub mod json_schema;
24pub mod kubectl;
25pub mod log_dedup;
26pub mod ls;
27pub mod make;
28pub mod maven;
29pub mod mix;
30pub mod mypy;
31pub mod mysql;
32pub mod next_build;
33pub mod npm;
34pub mod php;
35pub mod pip;
36pub mod playwright;
37pub mod pnpm;
38pub mod poetry;
39pub mod prettier;
40pub mod prisma;
41pub mod psql;
42pub mod ruby;
43pub mod ruff;
44pub mod swift;
45pub mod sysinfo;
46pub mod systemd;
47pub mod terraform;
48pub mod test;
49pub mod typescript;
50pub mod wget;
51pub mod zig;
52
53pub fn compress_output(command: &str, output: &str) -> Option<String> {
54    let cleaned = crate::core::compressor::strip_ansi(output);
55    let output = if cleaned.len() < output.len() {
56        &cleaned
57    } else {
58        output
59    };
60
61    if let Some(engine) = crate::core::filters::FilterEngine::load() {
62        if let Some(filtered) = engine.apply(command, output) {
63            return Some(filtered);
64        }
65    }
66
67    let specific = try_specific_pattern(command, output);
68    if specific.is_some() {
69        return specific;
70    }
71
72    if let Some(r) = json_schema::compress(output) {
73        return Some(r);
74    }
75
76    if let Some(r) = log_dedup::compress(output) {
77        return Some(r);
78    }
79
80    if let Some(r) = test::compress(output) {
81        return Some(r);
82    }
83
84    None
85}
86
87fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
88    let cl = cmd.to_ascii_lowercase();
89    let c = cl.as_str();
90
91    if c.starts_with("git ") {
92        return git::compress(c, output);
93    }
94    if c.starts_with("gh ") {
95        return gh::compress(c, output);
96    }
97    if c == "terraform" || c.starts_with("terraform ") {
98        return terraform::compress(c, output);
99    }
100    if c == "make" || c.starts_with("make ") {
101        return make::compress(c, output);
102    }
103    if c.starts_with("mvn ")
104        || c.starts_with("./mvnw ")
105        || c.starts_with("mvnw ")
106        || c.starts_with("gradle ")
107        || c.starts_with("./gradlew ")
108        || c.starts_with("gradlew ")
109    {
110        return maven::compress(c, output);
111    }
112    if c.starts_with("kubectl ") || c.starts_with("k ") {
113        return kubectl::compress(c, output);
114    }
115    if c.starts_with("helm ") {
116        return helm::compress(c, output);
117    }
118    if c.starts_with("pnpm ") {
119        return pnpm::compress(c, output);
120    }
121    if c.starts_with("bun ") || c.starts_with("bunx ") {
122        return bun::compress(c, output);
123    }
124    if c.starts_with("deno ") {
125        return deno::compress(c, output);
126    }
127    if c.starts_with("npm ") || c.starts_with("yarn ") {
128        return npm::compress(c, output);
129    }
130    if c.starts_with("cargo ") {
131        return cargo::compress(c, output);
132    }
133    if c.starts_with("docker ") || c.starts_with("docker-compose ") {
134        return docker::compress(c, output);
135    }
136    if c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip") {
137        return pip::compress(c, output);
138    }
139    if c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy ") {
140        return mypy::compress(c, output);
141    }
142    if c.starts_with("pytest") || c.starts_with("python -m pytest") {
143        return test::compress(output);
144    }
145    if c.starts_with("ruff ") {
146        return ruff::compress(c, output);
147    }
148    if c.starts_with("eslint")
149        || c.starts_with("npx eslint")
150        || c.starts_with("biome ")
151        || c.starts_with("stylelint")
152    {
153        return eslint::compress(c, output);
154    }
155    if c.starts_with("prettier") || c.starts_with("npx prettier") {
156        return prettier::compress(output);
157    }
158    if c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint") {
159        return golang::compress(c, output);
160    }
161    if c.starts_with("playwright")
162        || c.starts_with("npx playwright")
163        || c.starts_with("cypress")
164        || c.starts_with("npx cypress")
165    {
166        return playwright::compress(c, output);
167    }
168    if c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest") {
169        return test::compress(output);
170    }
171    if c.starts_with("next ")
172        || c.starts_with("npx next")
173        || c.starts_with("vite ")
174        || c.starts_with("npx vite")
175        || c.starts_with("vp ")
176        || c.starts_with("vite-plus ")
177    {
178        return next_build::compress(c, output);
179    }
180    if c.starts_with("tsc") || c.contains("typescript") {
181        return typescript::compress(output);
182    }
183    if c.starts_with("rubocop")
184        || c.starts_with("bundle ")
185        || c.starts_with("rake ")
186        || c.starts_with("rails test")
187        || c.starts_with("rspec")
188    {
189        return ruby::compress(c, output);
190    }
191    if c.starts_with("grep ") || c.starts_with("rg ") {
192        return grep::compress(output);
193    }
194    if c.starts_with("find ") {
195        return find::compress(output);
196    }
197    if c.starts_with("ls ") || c == "ls" {
198        return ls::compress(output);
199    }
200    if c.starts_with("curl ") {
201        return curl::compress_with_cmd(c, output);
202    }
203    if c.starts_with("wget ") {
204        return wget::compress(output);
205    }
206    if c == "env" || c.starts_with("env ") || c.starts_with("printenv") {
207        return env_filter::compress(output);
208    }
209    if c.starts_with("dotnet ") {
210        return dotnet::compress(c, output);
211    }
212    if c.starts_with("flutter ")
213        || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
214    {
215        return flutter::compress(c, output);
216    }
217    if c.starts_with("poetry ")
218        || c.starts_with("uv sync")
219        || (c.starts_with("uv ") && c.contains("pip install"))
220        || c.starts_with("conda ")
221        || c.starts_with("mamba ")
222        || c.starts_with("pipx ")
223    {
224        return poetry::compress(c, output);
225    }
226    if c.starts_with("aws ") {
227        return aws::compress(c, output);
228    }
229    if c.starts_with("psql ") || c.starts_with("pg_") {
230        return psql::compress(c, output);
231    }
232    if c.starts_with("mysql ") || c.starts_with("mariadb ") {
233        return mysql::compress(c, output);
234    }
235    if c.starts_with("prisma ") || c.starts_with("npx prisma") {
236        return prisma::compress(c, output);
237    }
238    if c.starts_with("swift ") {
239        return swift::compress(c, output);
240    }
241    if c.starts_with("zig ") {
242        return zig::compress(c, output);
243    }
244    if c.starts_with("cmake ") || c.starts_with("ctest") {
245        return cmake::compress(c, output);
246    }
247    if c.starts_with("ansible") || c.starts_with("ansible-playbook") {
248        return ansible::compress(c, output);
249    }
250    if c.starts_with("composer ") {
251        return composer::compress(c, output);
252    }
253    if c.starts_with("php artisan") || c.starts_with("artisan ") {
254        return artisan::compress(c, output);
255    }
256    if c.starts_with("./vendor/bin/pest") || c.starts_with("pest ") {
257        return artisan::compress("php artisan test", output);
258    }
259    if c.starts_with("mix ") || c.starts_with("iex ") {
260        return mix::compress(c, output);
261    }
262    if c.starts_with("bazel ") || c.starts_with("blaze ") {
263        return bazel::compress(c, output);
264    }
265    if c.starts_with("systemctl ") || c.starts_with("journalctl") {
266        return systemd::compress(c, output);
267    }
268    if c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest") {
269        return test::compress(output);
270    }
271    if c.starts_with("mocha") || c.starts_with("npx mocha") {
272        return test::compress(output);
273    }
274    if c.starts_with("tofu ") {
275        return terraform::compress(c, output);
276    }
277    if c.starts_with("ps ") || c == "ps" {
278        return sysinfo::compress_ps(output);
279    }
280    if c.starts_with("df ") || c == "df" {
281        return sysinfo::compress_df(output);
282    }
283    if c.starts_with("du ") || c == "du" {
284        return sysinfo::compress_du(output);
285    }
286    if c.starts_with("ping ") {
287        return sysinfo::compress_ping(output);
288    }
289    if c.starts_with("jq ") || c == "jq" {
290        return json_schema::compress(output);
291    }
292    if c.starts_with("hadolint") {
293        return eslint::compress(c, output);
294    }
295    if c.starts_with("yamllint") || c.starts_with("npx yamllint") {
296        return eslint::compress(c, output);
297    }
298    if c.starts_with("markdownlint") || c.starts_with("npx markdownlint") {
299        return eslint::compress(c, output);
300    }
301    if c.starts_with("oxlint") || c.starts_with("npx oxlint") {
302        return eslint::compress(c, output);
303    }
304    if c.starts_with("pyright") || c.starts_with("basedpyright") {
305        return mypy::compress(c, output);
306    }
307    if c.starts_with("turbo ") || c.starts_with("npx turbo") {
308        return npm::compress(c, output);
309    }
310    if c.starts_with("nx ") || c.starts_with("npx nx") {
311        return npm::compress(c, output);
312    }
313    if c.starts_with("gcc ")
314        || c.starts_with("g++ ")
315        || c.starts_with("cc ")
316        || c.starts_with("c++ ")
317    {
318        return cmake::compress(c, output);
319    }
320
321    None
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn routes_git_commands() {
330        let output = "On branch main\nnothing to commit";
331        assert!(compress_output("git status", output).is_some());
332    }
333
334    #[test]
335    fn routes_cargo_commands() {
336        let output = "   Compiling lean-ctx v2.1.1\n    Finished `release` profile [optimized] target(s) in 30.5s";
337        assert!(compress_output("cargo build --release", output).is_some());
338    }
339
340    #[test]
341    fn routes_npm_commands() {
342        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";
343        assert!(compress_output("npm install", output).is_some());
344    }
345
346    #[test]
347    fn routes_docker_commands() {
348        let output = "CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES";
349        assert!(compress_output("docker ps", output).is_some());
350    }
351
352    #[test]
353    fn routes_mypy_commands() {
354        let output = "src/main.py:10: error: Missing return  [return]\nFound 1 error in 1 file (checked 3 source files)";
355        assert!(compress_output("mypy .", output).is_some());
356        assert!(compress_output("python -m mypy src/", output).is_some());
357    }
358
359    #[test]
360    fn routes_pytest_commands() {
361        let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
362        assert!(compress_output("pytest", output).is_some());
363        assert!(compress_output("python -m pytest tests/", output).is_some());
364    }
365
366    #[test]
367    fn unknown_command_returns_none() {
368        assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
369    }
370
371    #[test]
372    fn case_insensitive_routing() {
373        let output = "On branch main\nnothing to commit";
374        assert!(compress_output("Git Status", output).is_some());
375        assert!(compress_output("GIT STATUS", output).is_some());
376    }
377
378    #[test]
379    fn routes_vp_and_vite_plus() {
380        let output = "  VITE v5.0.0  ready in 200 ms\n\n  -> Local:   http://localhost:5173/\n  -> Network: http://192.168.1.2:5173/";
381        assert!(compress_output("vp build", output).is_some());
382        assert!(compress_output("vite-plus build", output).is_some());
383    }
384
385    #[test]
386    fn routes_bunx_commands() {
387        let output = "1 pass tests\nDone 12ms";
388        let compressed = compress_output("bunx test", output).unwrap();
389        assert!(compressed.contains("bun test: 1 passed"));
390    }
391
392    #[test]
393    fn routes_deno_task() {
394        let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
395        assert!(try_specific_pattern("deno task dev", output).is_some());
396    }
397
398    #[test]
399    fn routes_jest_commands() {
400        let output = "PASS  tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests:       5 passed, 5 total\nTime:        2.5 s";
401        assert!(try_specific_pattern("jest", output).is_some());
402        assert!(try_specific_pattern("npx jest --coverage", output).is_some());
403    }
404
405    #[test]
406    fn routes_mocha_commands() {
407        let output = "  3 passing (50ms)\n  1 failing\n\n  1) Array #indexOf():\n     Error: expected -1 to equal 0";
408        assert!(try_specific_pattern("mocha", output).is_some());
409        assert!(try_specific_pattern("npx mocha tests/", output).is_some());
410    }
411
412    #[test]
413    fn routes_tofu_commands() {
414        let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
415        assert!(try_specific_pattern("tofu init", output).is_some());
416    }
417
418    #[test]
419    fn routes_ps_commands() {
420        let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
421        for i in 0..20 {
422            lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
423        }
424        let output = lines.join("\n");
425        assert!(try_specific_pattern("ps aux", &output).is_some());
426    }
427
428    #[test]
429    fn routes_ping_commands() {
430        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";
431        assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
432    }
433
434    #[test]
435    fn routes_jq_to_json_schema() {
436        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}]}";
437        assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
438    }
439
440    #[test]
441    fn routes_linting_tools() {
442        let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
443        assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
444        assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
445        assert!(try_specific_pattern("pyright src/", lint_output).is_some());
446        assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
447    }
448
449    #[test]
450    fn routes_build_tools() {
451        let build_output = "   Compiling foo v0.1.0\n    Finished release [optimized]";
452        assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
453        assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
454    }
455
456    #[test]
457    fn routes_monorepo_tools() {
458        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";
459        assert!(try_specific_pattern("turbo install", output).is_some());
460        assert!(try_specific_pattern("nx install", output).is_some());
461    }
462}