mirror of
https://github.com/rtk-ai/rtk.git
synced 2026-09-19 07:33:17 +08:00
feat: TOML Part 2 — user-global config, shadow warning, rtk init templates, 4 new built-in filters (#351)
* docs: update module count to 58 (toml_filter + verify_cmd) * docs: bump version refs to 0.27.1, fix module count to 59 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: TOML DSL PR 2 — user-global config, shadow warning, init templates, 4 new filters - toml_filter: add ~/.config/rtk/filters.toml (priority 2, between project and built-in) - toml_filter: shadow warning when match_command overlaps a Rust-handled command - init: rtk init generates .rtk/filters.toml template (local) and ~/.config/rtk/filters.toml (global) - builtin_filters: add pre-commit, helm, gcloud, ansible-playbook (46/46 inline tests) - README: add "Custom Filters" section with lookup table, primitives, examples, built-in list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(filters): add quarto-render, pnpm-build, trunk-build from rtk discover data (52/52 tests) * feat(filters): add docker-inspect, sops, docker-compose-ps built-in filters Resolves issues #279, #277, #276 as TOML-native filters (no Rust required). - docker-inspect: strip_ansi + truncate_lines_at=120 + max_lines=60 - sops: strip_ansi + strip_lines_matching blank lines + max_lines=40 - docker-compose-ps: strip_ansi + truncate_lines_at=120 + max_lines=40 6 inline tests added (2 per filter), all passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(filters): split monolithic builtin_filters.toml into 28 individual files Replace the 848-line src/builtin_filters.toml monolith with individual files under src/filters/<name>.toml (1 filter + tests per file). Files are concatenated alphabetically by build.rs at compile time into OUT_DIR/builtin_filters.toml, then embedded via a BUILTIN_TOML constant in toml_filter.rs. The build step validates TOML syntax and detects duplicate filter names across files. Benefits: - Zero merge conflicts when multiple PRs add filters (different files) - Clear PR diffs: "+1 file of 30 lines" vs "+30 lines in 850-line file" - Easy onboarding: copy any .toml, rename, edit 3 fields — done - Build-time TOML validation catches syntax errors before tests run Changes: - build.rs (new): concat + validate src/filters/*.toml - Cargo.toml: add [build-dependencies] toml = "0.8" - src/filters/*.toml (28 files): split from monolith - src/filters/README.md (new): contributor guide - src/toml_filter.rs: BUILTIN_TOML const + 5 include_str replacements - src/builtin_filters.toml: deleted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(filters): add 5 new unit tests for multi-file architecture + wget filter Unit tests added (toml_filter.rs): - test_builtin_toml_has_schema_version: ensures build.rs injects schema_version - test_builtin_all_expected_filters_present: guards against accidental file deletion - test_builtin_filter_count: exact count check (fails if filter added/removed without update) - test_builtin_all_filters_have_inline_tests: prevents shipping filters with no tests - test_new_filter_discoverable_after_concat: simulates build.rs concat, verifies lookup New built-in filter: - src/filters/wget.toml: compact wget download output (strips connection/resolution noise, short-circuits on 'saved [' to 'ok (downloaded)', 2 inline tests) Test results: 661/661 unit tests, 60/60 inline TOML filter tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add filter-workflow.md with Mermaid diagrams Build pipeline + runtime lookup priority, both as Mermaid flowcharts. Shows the full path from src/filters/*.toml to binary to execution. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove dead TOML filters — wget, git-checkout, git-merge, git-remote, cargo-run These filters were never reachable: Clap routes rtk git/cargo commands to their dedicated Rust modules before run_fallback is called. The TOML engine only fires for unknown commands. Shipping dead filters gives false confidence (inline tests pass, but filters never activate in production). Also fixes P1-1 from the PR #386 review: wget was in RUST_HANDLED_COMMANDS and the shadow warning fired at compile time but did not block the build. Removed: cargo-run.toml, git-checkout.toml, git-merge.toml, git-remote.toml, wget.toml Updated test guards: count 29→24, expected list -5 names, concat test 30→25. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove 3 dead TOML filters + update guards (PR #351) docker-inspect, docker-compose-ps, and pnpm-build are handled by container.rs and pnpm_cmd.rs before run_fallback is reached — their TOML filters never fire. Remove the files to avoid false documentation. Update toml_filter.rs guards: expected list -3 names, count 24→21, concat test 25→22. Update CONTRIBUTING.md example count to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: rebase on PR #349 — apply regex anchors, make on_empty, build.rs clippy, docs macOS path - Add (\\s|$) word-boundary anchors to 6 tofu/mix match_command regexes (tofu-plan, tofu-init, tofu-validate, tofu-fmt, mix-format, mix-compile) - Add on_empty = "make: ok" to make.toml + update existing empty-output test - Fix build.rs clippy: map_or(false, ...) → is_some_and(..) - Add macOS alt path note to filter-workflow.md Mermaid diagram Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -58,10 +58,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* **toml-dsl:** declarative TOML filter engine — add command filters without writing Rust ([#299](https://github.com/rtk-ai/rtk/issues/299))
|
||||
* 8 primitives: `strip_ansi`, `replace`, `match_output`, `strip/keep_lines_matching`, `truncate_lines_at`, `head/tail_lines`, `max_lines`, `on_empty`
|
||||
* lookup chain: `.rtk/filters.toml` (project-local) → built-in filters
|
||||
* lookup chain: `.rtk/filters.toml` (project-local) → `~/.config/rtk/filters.toml` (user-global) → built-in filters
|
||||
* `RTK_NO_TOML=1` bypass, `RTK_TOML_DEBUG=1` debug mode
|
||||
* shadow warning when a TOML filter's match_command overlaps a Rust-handled command
|
||||
* `rtk init` generates commented filter templates at both project and global level
|
||||
* `rtk verify` command with `--require-all` for inline test validation
|
||||
* 14 new built-in filters: `tofu-plan/init/validate/fmt` ([#240](https://github.com/rtk-ai/rtk/issues/240)), `du` ([#284](https://github.com/rtk-ai/rtk/issues/284)), `fail2ban-client` ([#281](https://github.com/rtk-ai/rtk/issues/281)), `iptables` ([#282](https://github.com/rtk-ai/rtk/issues/282)), `mix-format/compile` ([#310](https://github.com/rtk-ai/rtk/issues/310)), `shopify-theme` ([#280](https://github.com/rtk-ai/rtk/issues/280)), `pio-run` ([#231](https://github.com/rtk-ai/rtk/issues/231)), `mvn-build` ([#338](https://github.com/rtk-ai/rtk/issues/338))
|
||||
* 18 built-in filters: `tofu-plan/init/validate/fmt` ([#240](https://github.com/rtk-ai/rtk/issues/240)), `du` ([#284](https://github.com/rtk-ai/rtk/issues/284)), `fail2ban-client` ([#281](https://github.com/rtk-ai/rtk/issues/281)), `iptables` ([#282](https://github.com/rtk-ai/rtk/issues/282)), `mix-format/compile` ([#310](https://github.com/rtk-ai/rtk/issues/310)), `shopify-theme` ([#280](https://github.com/rtk-ai/rtk/issues/280)), `pio-run` ([#231](https://github.com/rtk-ai/rtk/issues/231)), `mvn-build` ([#338](https://github.com/rtk-ai/rtk/issues/338)), `pre-commit`, `helm`, `gcloud`, `ansible-playbook`
|
||||
* **hooks:** `exclude_commands` config — exclude specific commands from auto-rewrite ([#243](https://github.com/rtk-ai/rtk/issues/243))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -31,6 +31,9 @@ sha2 = "0.10"
|
||||
ureq = "2"
|
||||
hostname = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
toml = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://github.com/rtk-ai/rtk/actions"><img src="https://github.com/rtk-ai/rtk/workflows/Security%20Check/badge.svg" alt="CI"></a>
|
||||
<a href="https://github.com/rtk-ai/rtk/releases"><img src="https://img.shields.io/github/v/release/rtk-ai/rtk" alt="Release"></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||
<a href="https://discord.gg/gFwRPEKq4p"><img src="https://img.shields.io/discord/1478373640461488159?label=Discord&logo=discord" alt="Discord"></a>
|
||||
<a href="https://discord.gg/pvHdzAec"><img src="https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord" alt="Discord"></a>
|
||||
<a href="https://formulae.brew.sh/formula/rtk"><img src="https://img.shields.io/homebrew/v/rtk" alt="Homebrew"></a>
|
||||
</p>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<a href="#installation">Install</a> •
|
||||
<a href="docs/TROUBLESHOOTING.md">Troubleshooting</a> •
|
||||
<a href="ARCHITECTURE.md">Architecture</a> •
|
||||
<a href="https://discord.gg/gFwRPEKq4p">Discord</a>
|
||||
<a href="https://discord.gg/pvHdzAec">Discord</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -351,19 +351,12 @@ brew uninstall rtk # If installed via Homebrew
|
||||
- **[SECURITY.md](SECURITY.md)** - Security policy and PR review process
|
||||
- **[AUDIT_GUIDE.md](docs/AUDIT_GUIDE.md)** - Token savings analytics guide
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! See the **[Contributing Guide](CONTRIBUTING.md)** for branch naming, PR process, testing requirements, and coding practices.
|
||||
Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/rtk-ai/rtk).
|
||||
|
||||
Join the community on [Discord](https://discord.gg/pvHdzAec).
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contact
|
||||
|
||||
- Website: https://www.rtk-ai.app
|
||||
- Email: contact@rtk-ai.app
|
||||
- Issues: https://github.com/rtk-ai/rtk/issues
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let filters_dir = Path::new("src/filters");
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR must be set by Cargo");
|
||||
let dest = Path::new(&out_dir).join("builtin_filters.toml");
|
||||
|
||||
// Rebuild when any file in src/filters/ changes
|
||||
println!("cargo:rerun-if-changed=src/filters");
|
||||
|
||||
let mut files: Vec<_> = fs::read_dir(filters_dir)
|
||||
.expect("src/filters/ directory must exist")
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
|
||||
.collect();
|
||||
|
||||
// Sort alphabetically for deterministic filter ordering
|
||||
files.sort_by_key(|e| e.file_name());
|
||||
|
||||
let mut combined = String::from("schema_version = 1\n\n");
|
||||
|
||||
for entry in &files {
|
||||
let content = fs::read_to_string(entry.path())
|
||||
.unwrap_or_else(|e| panic!("Failed to read {:?}: {}", entry.path(), e));
|
||||
combined.push_str(&format!(
|
||||
"# --- {} ---\n",
|
||||
entry.file_name().to_string_lossy()
|
||||
));
|
||||
combined.push_str(&content);
|
||||
combined.push_str("\n\n");
|
||||
}
|
||||
|
||||
// Validate: parse the combined TOML to catch errors at build time
|
||||
let parsed: toml::Value = combined.parse().unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"TOML validation failed for combined filters:\n{}\n\nCheck src/filters/*.toml files",
|
||||
e
|
||||
)
|
||||
});
|
||||
|
||||
// Detect duplicate filter names across files
|
||||
if let Some(filters) = parsed.get("filters").and_then(|f| f.as_table()) {
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
for key in filters.keys() {
|
||||
if !seen.insert(key.clone()) {
|
||||
panic!(
|
||||
"Duplicate filter name '{}' found across src/filters/*.toml files",
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&dest, combined).expect("Failed to write combined builtin_filters.toml");
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# How a TOML filter goes from file to execution
|
||||
|
||||
This document explains what happens between "I created `src/filters/my-tool.toml`" and "RTK filters the output of `my-tool`".
|
||||
|
||||
## Build pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[["📄 src/filters/my-tool.toml\n(new file)"]] --> B
|
||||
|
||||
subgraph BUILD ["🔨 cargo build"]
|
||||
B["build.rs\n① ls src/filters/*.toml\n② sort alphabetically\n③ concat → schema_version = 1 + all files"] --> C
|
||||
C{"TOML valid?\nDuplicate names?"} -->|"❌ panic! (build fails)"| D[["🛑 Error message\npoints to bad file"]]
|
||||
C -->|"✅ ok"| E[["OUT_DIR/builtin_filters.toml\n(generated file)"]]
|
||||
E --> F["rustc\ninclude_str!(concat!(env!(OUT_DIR),\n'/builtin_filters.toml'))"]
|
||||
F --> G[["🦀 rtk binary\nBUILTIN_TOML embedded"]]
|
||||
end
|
||||
|
||||
subgraph TESTS ["🧪 cargo test"]
|
||||
H["test_builtin_filter_count\nassert_eq!(filters.len(), N)"] -->|"❌ count wrong"| I[["FAIL\n'Expected N, got N+1'\nUpdate the count'"]]
|
||||
J["test_builtin_all_expected_\nfilters_present\nassert!(names.contains('my-tool'))"] -->|"❌ name missing"| K[["FAIL\n'my-tool is missing—\nwas its .toml deleted?'"]]
|
||||
L["test_builtin_all_filters_\nhave_inline_tests\nassert!(tested.contains(name))"] -->|"❌ no tests"| M[["FAIL\n'Add tests.my-tool\nentries'"]]
|
||||
end
|
||||
|
||||
subgraph VERIFY ["✅ rtk verify"]
|
||||
N["runs [[tests.my-tool]]\ninput → filter → compare expected"]
|
||||
N -->|"❌ mismatch"| O[["FAIL\nshows actual vs expected"]]
|
||||
N -->|"✅ pass"| P[["60/60 tests passed"]]
|
||||
end
|
||||
|
||||
G --> H
|
||||
G --> J
|
||||
G --> L
|
||||
G --> N
|
||||
|
||||
subgraph RUNTIME ["⚡ rtk my-tool --verbose"]
|
||||
Q["Claude Code hook\nmy-tool ... → rtk my-tool ..."] --> R
|
||||
R["TomlFilterRegistry::load()\n① .rtk/filters.toml (project)\n② ~/.config/rtk/filters.toml (user)\n③ BUILTIN_TOML (binary)\n④ passthrough"] --> S
|
||||
S{"match_command\n'^my-tool\\b'\nmatches?"} -->|"No match"| T[["exec raw\n(passthrough)"]]
|
||||
S -->|"✅ match"| U["exec command\ncapture stdout"]
|
||||
U --> V
|
||||
|
||||
subgraph PIPELINE ["8-stage filter pipeline"]
|
||||
V["strip_ansi"] --> W["replace"]
|
||||
W --> X{"match_output\nshort-circuit?"}
|
||||
X -->|"✅ pattern matched"| Y[["emit message\nstop pipeline"]]
|
||||
X -->|"no match"| Z["strip/keep_lines"]
|
||||
Z --> AA["truncate_lines_at"]
|
||||
AA --> AB["tail_lines"]
|
||||
AB --> AC["max_lines"]
|
||||
AC --> AD{"output\nempty?"}
|
||||
AD -->|"yes"| AE[["emit on_empty"]]
|
||||
AD -->|"no"| AF[["print filtered\noutput + exit code"]]
|
||||
end
|
||||
end
|
||||
|
||||
G --> Q
|
||||
|
||||
style BUILD fill:#1e3a5f,color:#fff
|
||||
style TESTS fill:#1a3a1a,color:#fff
|
||||
style VERIFY fill:#2d1b69,color:#fff
|
||||
style RUNTIME fill:#3a1a1a,color:#fff
|
||||
style PIPELINE fill:#4a2a00,color:#fff
|
||||
style D fill:#8b0000,color:#fff
|
||||
style I fill:#8b0000,color:#fff
|
||||
style K fill:#8b0000,color:#fff
|
||||
style M fill:#8b0000,color:#fff
|
||||
style O fill:#8b0000,color:#fff
|
||||
```
|
||||
|
||||
## Step-by-step summary
|
||||
|
||||
| Step | Who | What happens | Fails if |
|
||||
|------|-----|--------------|----------|
|
||||
| 1 | Contributor | Creates `src/filters/my-tool.toml` | — |
|
||||
| 2 | `build.rs` | Concatenates all `.toml` files alphabetically | TOML syntax error, duplicate filter name |
|
||||
| 3 | `rustc` | Embeds result in binary via `BUILTIN_TOML` const | — |
|
||||
| 4 | `cargo test` | 3 guards check count, names, inline test presence | Count not updated, name not in list, no `[[tests.*]]` |
|
||||
| 5 | `rtk verify` | Runs each `[[tests.my-tool]]` entry | Filter logic doesn't match expected output |
|
||||
| 6 | Runtime | Hook rewrites command, registry looks up filter, pipeline runs | No match → passthrough (not an error) |
|
||||
|
||||
## Filter lookup priority at runtime
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CMD["rtk my-tool args"] --> P1
|
||||
P1{"1. .rtk/filters.toml\n(project-local)"}
|
||||
P1 -->|"✅ match"| WIN["apply filter"]
|
||||
P1 -->|"no match"| P2
|
||||
P2{"2. ~/.config/rtk/filters.toml\n(user-global)\n(macOS alt: ~/Library/Application Support/rtk/filters.toml)"}
|
||||
P2 -->|"✅ match"| WIN
|
||||
P2 -->|"no match"| P3
|
||||
P3{"3. BUILTIN_TOML\n(binary)"}
|
||||
P3 -->|"✅ match"| WIN
|
||||
P3 -->|"no match"| P4[["exec raw\n(passthrough)"]]
|
||||
```
|
||||
|
||||
First match wins. A project filter with the same name as a built-in shadows the built-in and triggers a warning:
|
||||
|
||||
```
|
||||
[rtk] warning: filter 'make' is shadowing a built-in filter
|
||||
```
|
||||
@@ -1,450 +0,0 @@
|
||||
schema_version = 1
|
||||
|
||||
[filters.make]
|
||||
description = "Compact make output"
|
||||
match_command = "^make\\b"
|
||||
strip_lines_matching = [
|
||||
"^make\\[\\d+\\]:",
|
||||
"^\\s*$",
|
||||
"^Nothing to be done",
|
||||
]
|
||||
max_lines = 50
|
||||
on_empty = "make: ok"
|
||||
|
||||
[filters.terraform-plan]
|
||||
description = "Compact Terraform plan output"
|
||||
match_command = "^terraform\\s+plan(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Refreshing state",
|
||||
"^\\s*#.*unchanged",
|
||||
"^\\s*$",
|
||||
"^Acquiring state lock",
|
||||
"^Releasing state lock",
|
||||
]
|
||||
max_lines = 80
|
||||
on_empty = "terraform plan: no changes detected"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inline tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[[tests.terraform-plan]]
|
||||
name = "strips Refreshing state lines and blank lines"
|
||||
input = """
|
||||
Acquiring state lock. This may take a few moments...
|
||||
Refreshing state... [id=vpc-abc]
|
||||
Refreshing state... [id=sg-123]
|
||||
Releasing state lock. This may take a few moments...
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# aws_instance.web will be created
|
||||
+ resource "aws_instance" "web" {}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
"""
|
||||
expected = "Terraform will perform the following actions:\n # aws_instance.web will be created\n + resource \"aws_instance\" \"web\" {}\nPlan: 1 to add, 0 to change, 0 to destroy."
|
||||
|
||||
[[tests.terraform-plan]]
|
||||
name = "strips noise, preserves non-blank content"
|
||||
input = "Refreshing state... [id=vpc-abc]\nNo changes. Your infrastructure matches the configuration."
|
||||
expected = "No changes. Your infrastructure matches the configuration."
|
||||
|
||||
|
||||
[[tests.make]]
|
||||
name = "strips entering/leaving lines"
|
||||
input = """
|
||||
make[1]: Entering directory '/home/user'
|
||||
gcc -O2 foo.c
|
||||
make[1]: Leaving directory '/home/user'
|
||||
"""
|
||||
expected = """
|
||||
gcc -O2 foo.c
|
||||
"""
|
||||
|
||||
[[tests.make]]
|
||||
name = "strips blank lines"
|
||||
input = """
|
||||
gcc -O2 foo.c
|
||||
|
||||
gcc -O2 bar.c
|
||||
"""
|
||||
expected = """
|
||||
gcc -O2 foo.c
|
||||
gcc -O2 bar.c
|
||||
"""
|
||||
|
||||
[[tests.make]]
|
||||
name = "on_empty when all stripped"
|
||||
input = "make[1]: Entering directory '/home/user'\nmake[1]: Nothing to be done for 'all'.\nmake[1]: Leaving directory '/home/user'\n"
|
||||
expected = "make: ok"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #240 OpenTofu (mirror of Terraform filters)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.tofu-plan]
|
||||
description = "Compact OpenTofu plan output"
|
||||
match_command = "^tofu\\s+plan(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Refreshing state",
|
||||
"^\\s*#.*unchanged",
|
||||
"^\\s*$",
|
||||
"^Acquiring state lock",
|
||||
"^Releasing state lock",
|
||||
]
|
||||
max_lines = 80
|
||||
on_empty = "tofu plan: no changes detected"
|
||||
|
||||
[filters.tofu-init]
|
||||
description = "Compact OpenTofu init output"
|
||||
match_command = "^tofu\\s+init(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^- Downloading",
|
||||
"^- Installing",
|
||||
"^- Using previously-installed",
|
||||
"^\\s*$",
|
||||
"^Initializing provider",
|
||||
"^Initializing the backend",
|
||||
"^Initializing modules",
|
||||
]
|
||||
max_lines = 20
|
||||
on_empty = "tofu init: ok"
|
||||
|
||||
[filters.tofu-validate]
|
||||
description = "Compact OpenTofu validate output"
|
||||
match_command = "^tofu\\s+validate(\\s|$)"
|
||||
strip_ansi = true
|
||||
match_output = [
|
||||
{ pattern = "Success! The configuration is valid", message = "ok (valid)" },
|
||||
]
|
||||
|
||||
[filters.tofu-fmt]
|
||||
description = "Compact OpenTofu fmt output"
|
||||
match_command = "^tofu\\s+fmt(\\s|$)"
|
||||
strip_ansi = true
|
||||
on_empty = "tofu fmt: ok (no changes)"
|
||||
max_lines = 30
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #284 du
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.du]
|
||||
description = "Compact du output"
|
||||
match_command = "^du\\b"
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
truncate_lines_at = 120
|
||||
max_lines = 40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #281 fail2ban-client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.fail2ban-client]
|
||||
description = "Compact fail2ban-client output"
|
||||
match_command = "^fail2ban-client\\b"
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
max_lines = 30
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #282 iptables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.iptables]
|
||||
description = "Compact iptables output"
|
||||
match_command = "^iptables(\\s|$)"
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^Chain DOCKER",
|
||||
"^Chain BR-",
|
||||
]
|
||||
max_lines = 50
|
||||
truncate_lines_at = 120
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #310 Elixir mix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.mix-format]
|
||||
description = "Compact mix format output"
|
||||
match_command = "^mix\\s+format(\\s|$)"
|
||||
on_empty = "mix format: ok"
|
||||
max_lines = 20
|
||||
|
||||
[filters.mix-compile]
|
||||
description = "Compact mix compile output"
|
||||
match_command = "^mix\\s+compile(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Compiling \\d+ file",
|
||||
"^\\s*$",
|
||||
"^Generated\\s",
|
||||
]
|
||||
max_lines = 40
|
||||
on_empty = "mix compile: ok"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #280 Shopify theme
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.shopify-theme]
|
||||
description = "Compact shopify theme push/pull output"
|
||||
match_command = "^shopify\\s+theme\\s+(push|pull)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^\\s*Uploading",
|
||||
"^\\s*Downloading",
|
||||
]
|
||||
tail_lines = 5
|
||||
max_lines = 15
|
||||
on_empty = "shopify theme: ok"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #231 PlatformIO
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.pio-run]
|
||||
description = "Compact PlatformIO build output"
|
||||
match_command = "^pio\\s+run"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^Verbose mode",
|
||||
"^CONFIGURATION:",
|
||||
"^LDF:",
|
||||
"^Library Manager:",
|
||||
"^Compiling\\s",
|
||||
"^Linking\\s",
|
||||
"^Building\\s",
|
||||
"^Checking size",
|
||||
]
|
||||
max_lines = 30
|
||||
on_empty = "pio run: ok"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #338 Maven
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[filters.mvn-build]
|
||||
description = "Compact Maven build output"
|
||||
match_command = "^mvn\\s+(compile|package|clean|install)\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\[INFO\\] ---",
|
||||
"^\\[INFO\\] Building\\s",
|
||||
"^\\[INFO\\] Downloading\\s",
|
||||
"^\\[INFO\\] Downloaded\\s",
|
||||
"^\\[INFO\\]\\s*$",
|
||||
"^\\s*$",
|
||||
"^Downloading:",
|
||||
"^Downloaded:",
|
||||
"^Progress",
|
||||
]
|
||||
max_lines = 50
|
||||
on_empty = "mvn: ok"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inline tests — new filters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[[tests.tofu-plan]]
|
||||
name = "strips Refreshing state and lock lines"
|
||||
input = """
|
||||
Acquiring state lock. This may take a few moments...
|
||||
Refreshing state... [id=vpc-abc123]
|
||||
Refreshing state... [id=sg-def456]
|
||||
Releasing state lock. This may take a few moments...
|
||||
|
||||
OpenTofu will perform the following actions:
|
||||
|
||||
# aws_instance.web will be created
|
||||
+ resource "aws_instance" "web" {}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
"""
|
||||
expected = "OpenTofu will perform the following actions:\n # aws_instance.web will be created\n + resource \"aws_instance\" \"web\" {}\nPlan: 1 to add, 0 to change, 0 to destroy."
|
||||
|
||||
[[tests.tofu-plan]]
|
||||
name = "on_empty when all noise stripped"
|
||||
input = "Refreshing state... [id=vpc-abc]\nAcquiring state lock. This may take a few moments...\nReleasing state lock. This may take a few moments..."
|
||||
expected = "tofu plan: no changes detected"
|
||||
|
||||
[[tests.tofu-init]]
|
||||
name = "strips downloading/installing lines"
|
||||
input = """
|
||||
Initializing the backend...
|
||||
Initializing provider plugins...
|
||||
- Downloading hashicorp/aws 5.0.0...
|
||||
- Installing hashicorp/aws 5.0.0...
|
||||
- Using previously-installed hashicorp/random 3.5.1
|
||||
|
||||
OpenTofu has been successfully initialized!
|
||||
"""
|
||||
expected = "OpenTofu has been successfully initialized!"
|
||||
|
||||
[[tests.tofu-init]]
|
||||
name = "on_empty when all noise stripped"
|
||||
input = """
|
||||
Initializing the backend...
|
||||
Initializing provider plugins...
|
||||
- Using previously-installed hashicorp/aws 5.0.0
|
||||
|
||||
"""
|
||||
expected = "tofu init: ok"
|
||||
|
||||
[[tests.tofu-validate]]
|
||||
name = "success short-circuits to ok"
|
||||
input = "Success! The configuration is valid."
|
||||
expected = "ok (valid)"
|
||||
|
||||
[[tests.tofu-validate]]
|
||||
name = "error passes through unchanged"
|
||||
input = "Error: Invalid resource type\n on main.tf line 3: resource \"aws_instancee\" \"web\""
|
||||
expected = "Error: Invalid resource type\n on main.tf line 3: resource \"aws_instancee\" \"web\""
|
||||
|
||||
[[tests.tofu-fmt]]
|
||||
name = "empty output returns on_empty message"
|
||||
input = ""
|
||||
expected = "tofu fmt: ok (no changes)"
|
||||
|
||||
[[tests.tofu-fmt]]
|
||||
name = "changed files pass through"
|
||||
input = "main.tf\nvariables.tf"
|
||||
expected = "main.tf\nvariables.tf"
|
||||
|
||||
[[tests.du]]
|
||||
name = "preserves sizes, strips blank lines"
|
||||
input = "4.0K\t./src\n\n8.0K\t./tests\n16K\t."
|
||||
expected = "4.0K\t./src\n8.0K\t./tests\n16K\t."
|
||||
|
||||
[[tests.du]]
|
||||
name = "single line passthrough"
|
||||
input = "128K\t."
|
||||
expected = "128K\t."
|
||||
|
||||
[[tests.fail2ban-client]]
|
||||
name = "strips blank lines"
|
||||
input = "Status for the jail: sshd\n|- Filter\n| |- Currently failed: 3\n\n|- Actions\n `- Total banned: 42"
|
||||
expected = "Status for the jail: sshd\n|- Filter\n| |- Currently failed: 3\n|- Actions\n `- Total banned: 42"
|
||||
|
||||
[[tests.fail2ban-client]]
|
||||
name = "single line passthrough"
|
||||
input = "Shutdown successful"
|
||||
expected = "Shutdown successful"
|
||||
|
||||
[[tests.iptables]]
|
||||
name = "strips Docker chains, preserves real rules"
|
||||
input = """
|
||||
Chain INPUT (policy ACCEPT)
|
||||
num target prot opt source destination
|
||||
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
|
||||
Chain DOCKER (1 references)
|
||||
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
|
||||
Chain BR-abcdef (0 references)
|
||||
"""
|
||||
expected = "Chain INPUT (policy ACCEPT)\nnum target prot opt source destination\n1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0\n DOCKER all -- 0.0.0.0/0 0.0.0.0/0"
|
||||
|
||||
[[tests.iptables]]
|
||||
name = "preserves FORWARD and OUTPUT chains"
|
||||
input = "Chain FORWARD (policy DROP)\n1 ACCEPT tcp\nChain OUTPUT (policy ACCEPT)\n1 ACCEPT all"
|
||||
expected = "Chain FORWARD (policy DROP)\n1 ACCEPT tcp\nChain OUTPUT (policy ACCEPT)\n1 ACCEPT all"
|
||||
|
||||
[[tests.mix-format]]
|
||||
name = "empty output returns ok"
|
||||
input = ""
|
||||
expected = "mix format: ok"
|
||||
|
||||
[[tests.mix-format]]
|
||||
name = "changed files pass through"
|
||||
input = "lib/my_app.ex\ntest/my_app_test.exs"
|
||||
expected = "lib/my_app.ex\ntest/my_app_test.exs"
|
||||
|
||||
[[tests.mix-compile]]
|
||||
name = "strips compile noise, preserves warnings"
|
||||
input = """
|
||||
Compiling 12 files (.ex)
|
||||
Generated my_app app
|
||||
|
||||
warning: variable "conn" is unused
|
||||
lib/router.ex:42
|
||||
"""
|
||||
expected = "warning: variable \"conn\" is unused\n lib/router.ex:42"
|
||||
|
||||
[[tests.mix-compile]]
|
||||
name = "on_empty when only noise"
|
||||
input = "Compiling 3 files (.ex)\nGenerated my_app app\n"
|
||||
expected = "mix compile: ok"
|
||||
|
||||
[[tests.shopify-theme]]
|
||||
name = "strips upload/download lines, keeps tail"
|
||||
input = """
|
||||
Uploading assets/app.css
|
||||
Uploading assets/app.js
|
||||
Uploading templates/index.liquid
|
||||
Downloading assets/old.css
|
||||
|
||||
Theme 'Development' (id: 12345) pushed to store.example.myshopify.com
|
||||
"""
|
||||
expected = "Theme 'Development' (id: 12345) pushed to store.example.myshopify.com"
|
||||
|
||||
[[tests.shopify-theme]]
|
||||
name = "on_empty when all stripped"
|
||||
input = "Uploading assets/app.css\nDownloading assets/base.css\n"
|
||||
expected = "shopify theme: ok"
|
||||
|
||||
[[tests.pio-run]]
|
||||
name = "strips build noise, preserves errors"
|
||||
input = """
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32dev.html
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
Compiling .pio/build/esp32dev/src/main.cpp.o
|
||||
Building .pio/build/esp32dev/firmware.elf
|
||||
Linking .pio/build/esp32dev/firmware.elf
|
||||
Checking size .pio/build/esp32dev/firmware.elf
|
||||
src/main.cpp:10:3: error: 'LED_BUILTINN' was not declared
|
||||
"""
|
||||
expected = "src/main.cpp:10:3: error: 'LED_BUILTINN' was not declared"
|
||||
|
||||
[[tests.pio-run]]
|
||||
name = "on_empty when clean build with only noise"
|
||||
input = """
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
Compiling .pio/build/esp32dev/src/main.cpp.o
|
||||
Linking .pio/build/esp32dev/firmware.elf
|
||||
"""
|
||||
expected = "pio run: ok"
|
||||
|
||||
[[tests.mvn-build]]
|
||||
name = "strips INFO noise, preserves errors and summary"
|
||||
input = """
|
||||
[INFO] ---
|
||||
[INFO] Building myapp 1.0-SNAPSHOT
|
||||
[INFO] Downloading org.apache.maven.plugins:maven-compiler-plugin:3.11.0
|
||||
[INFO] Downloaded org.apache.maven.plugins:maven-compiler-plugin:3.11.0
|
||||
[INFO]
|
||||
[ERROR] /src/main/java/Main.java:[10,5] cannot find symbol
|
||||
symbol: method foo()
|
||||
[INFO] BUILD FAILURE
|
||||
[INFO] Total time: 2.543 s
|
||||
"""
|
||||
expected = "[ERROR] /src/main/java/Main.java:[10,5] cannot find symbol\n symbol: method foo()\n[INFO] BUILD FAILURE\n[INFO] Total time: 2.543 s"
|
||||
|
||||
[[tests.mvn-build]]
|
||||
name = "successful build keeps BUILD SUCCESS line"
|
||||
input = """
|
||||
[INFO] ---
|
||||
[INFO] Building myapp 1.0-SNAPSHOT
|
||||
[INFO]
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 4.123 s
|
||||
[INFO] Finished at: 2024-01-15T10:30:00Z
|
||||
"""
|
||||
expected = "[INFO] BUILD SUCCESS\n[INFO] Total time: 4.123 s\n[INFO] Finished at: 2024-01-15T10:30:00Z"
|
||||
@@ -0,0 +1,52 @@
|
||||
# Built-in Filters
|
||||
|
||||
Each `.toml` file in this directory defines one filter and its inline tests.
|
||||
Files are concatenated alphabetically by `build.rs` into a single TOML blob embedded in the binary.
|
||||
|
||||
## Adding a filter
|
||||
|
||||
1. Copy any existing `.toml` file and rename it (e.g. `my-tool.toml`)
|
||||
2. Update the three required fields: `description`, `match_command`, and at least one action field
|
||||
3. Add `[[tests.my-tool]]` entries to verify the filter behaves correctly
|
||||
4. Run `cargo test` — the build step validates TOML syntax and runs inline tests
|
||||
|
||||
## File format
|
||||
|
||||
```toml
|
||||
[filters.my-tool]
|
||||
description = "Short description of what this filter does"
|
||||
match_command = "^my-tool\\b" # regex matched against the full command string
|
||||
strip_ansi = true # optional: strip ANSI escape codes first
|
||||
strip_lines_matching = [ # optional: drop lines matching any of these regexes
|
||||
"^\\s*$",
|
||||
"^noise pattern",
|
||||
]
|
||||
max_lines = 40 # optional: keep only the first N lines after filtering
|
||||
on_empty = "my-tool: ok" # optional: message to emit when output is empty after filtering
|
||||
|
||||
[[tests.my-tool]]
|
||||
name = "descriptive test name"
|
||||
input = "raw command output here"
|
||||
expected = "expected filtered output"
|
||||
```
|
||||
|
||||
## Available filter fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `description` | string | Human-readable description |
|
||||
| `match_command` | regex | Matches the command string (e.g. `"^docker\\s+inspect"`) |
|
||||
| `strip_ansi` | bool | Strip ANSI escape codes before processing |
|
||||
| `strip_lines_matching` | regex[] | Drop lines matching any regex |
|
||||
| `keep_lines_matching` | regex[] | Keep only lines matching at least one regex |
|
||||
| `replace` | array | Regex substitutions (`{ pattern, replacement }`) |
|
||||
| `match_output` | array | Short-circuit rules (`{ pattern, message }`) |
|
||||
| `truncate_lines_at` | int | Truncate lines longer than N characters |
|
||||
| `max_lines` | int | Keep only the first N lines |
|
||||
| `tail_lines` | int | Keep only the last N lines (applied after other filters) |
|
||||
| `on_empty` | string | Fallback message when filtered output is empty |
|
||||
|
||||
## Naming convention
|
||||
|
||||
Use the command name as the filename: `terraform-plan.toml`, `docker-inspect.toml`, `mix-compile.toml`.
|
||||
For commands with subcommands, prefer `<cmd>-<subcommand>.toml` over grouping multiple filters in one file.
|
||||
@@ -0,0 +1,34 @@
|
||||
[filters.ansible-playbook]
|
||||
description = "Compact ansible-playbook output"
|
||||
match_command = "^ansible-playbook\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^ok: \\[",
|
||||
"^skipping: \\[",
|
||||
]
|
||||
max_lines = 60
|
||||
|
||||
[[tests.ansible-playbook]]
|
||||
name = "strips ok and skipping lines, keeps changed and failures"
|
||||
input = """
|
||||
PLAY [all] *********************************************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [web01]
|
||||
ok: [web02]
|
||||
|
||||
TASK [Install nginx] ***********************************************************
|
||||
changed: [web01]
|
||||
skipping: [web02]
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
web01 : ok=2 changed=1 unreachable=0 failed=0
|
||||
web02 : ok=1 changed=0 unreachable=0 failed=0
|
||||
"""
|
||||
expected = "PLAY [all] *********************************************************************\nTASK [Gathering Facts] *********************************************************\nTASK [Install nginx] ***********************************************************\nchanged: [web01]\nPLAY RECAP *********************************************************************\nweb01 : ok=2 changed=1 unreachable=0 failed=0\nweb02 : ok=1 changed=0 unreachable=0 failed=0"
|
||||
|
||||
[[tests.ansible-playbook]]
|
||||
name = "failed task preserved"
|
||||
input = "TASK [Start service] ***\nfailed: [web01] => {\"msg\": \"Service not found\"}\nPLAY RECAP ***\nweb01 : ok=1 failed=1"
|
||||
expected = "TASK [Start service] ***\nfailed: [web01] => {\"msg\": \"Service not found\"}\nPLAY RECAP ***\nweb01 : ok=1 failed=1"
|
||||
@@ -0,0 +1,16 @@
|
||||
[filters.du]
|
||||
description = "Compact du output"
|
||||
match_command = "^du\\b"
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
truncate_lines_at = 120
|
||||
max_lines = 40
|
||||
|
||||
[[tests.du]]
|
||||
name = "preserves sizes, strips blank lines"
|
||||
input = "4.0K\t./src\n\n8.0K\t./tests\n16K\t."
|
||||
expected = "4.0K\t./src\n8.0K\t./tests\n16K\t."
|
||||
|
||||
[[tests.du]]
|
||||
name = "single line passthrough"
|
||||
input = "128K\t."
|
||||
expected = "128K\t."
|
||||
@@ -0,0 +1,15 @@
|
||||
[filters.fail2ban-client]
|
||||
description = "Compact fail2ban-client output"
|
||||
match_command = "^fail2ban-client\\b"
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
max_lines = 30
|
||||
|
||||
[[tests.fail2ban-client]]
|
||||
name = "strips blank lines"
|
||||
input = "Status for the jail: sshd\n|- Filter\n| |- Currently failed: 3\n\n|- Actions\n `- Total banned: 42"
|
||||
expected = "Status for the jail: sshd\n|- Filter\n| |- Currently failed: 3\n|- Actions\n `- Total banned: 42"
|
||||
|
||||
[[tests.fail2ban-client]]
|
||||
name = "single line passthrough"
|
||||
input = "Shutdown successful"
|
||||
expected = "Shutdown successful"
|
||||
@@ -0,0 +1,22 @@
|
||||
[filters.gcloud]
|
||||
description = "Compact gcloud output"
|
||||
match_command = "^gcloud\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
truncate_lines_at = 120
|
||||
max_lines = 30
|
||||
|
||||
[[tests.gcloud]]
|
||||
name = "strips blank lines, preserves output"
|
||||
input = """
|
||||
Updated property [core/project].
|
||||
|
||||
NAME REGION STATUS
|
||||
my-cluster us-central1 RUNNING
|
||||
"""
|
||||
expected = "Updated property [core/project].\nNAME REGION STATUS\nmy-cluster us-central1 RUNNING"
|
||||
|
||||
[[tests.gcloud]]
|
||||
name = "single line passthrough"
|
||||
input = "Listed 0 items."
|
||||
expected = "Listed 0 items."
|
||||
@@ -0,0 +1,29 @@
|
||||
[filters.helm]
|
||||
description = "Compact helm output"
|
||||
match_command = "^helm\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^W\\d{4}",
|
||||
]
|
||||
truncate_lines_at = 120
|
||||
max_lines = 40
|
||||
|
||||
[[tests.helm]]
|
||||
name = "strips blank lines, preserves release info"
|
||||
input = """
|
||||
NAME: my-release
|
||||
LAST DEPLOYED: Mon Jan 15 10:30:00 2024
|
||||
NAMESPACE: default
|
||||
STATUS: deployed
|
||||
REVISION: 3
|
||||
|
||||
NOTES:
|
||||
Application is running.
|
||||
"""
|
||||
expected = "NAME: my-release\nLAST DEPLOYED: Mon Jan 15 10:30:00 2024\nNAMESPACE: default\nSTATUS: deployed\nREVISION: 3\nNOTES:\nApplication is running."
|
||||
|
||||
[[tests.helm]]
|
||||
name = "strips glog W-prefix warnings"
|
||||
input = "W0115 10:30:00 warning message from internal\nNAME: my-chart\nSTATUS: deployed"
|
||||
expected = "NAME: my-chart\nSTATUS: deployed"
|
||||
@@ -0,0 +1,27 @@
|
||||
[filters.iptables]
|
||||
description = "Compact iptables output"
|
||||
match_command = "^iptables\\b"
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^Chain DOCKER",
|
||||
"^Chain BR-",
|
||||
]
|
||||
max_lines = 50
|
||||
truncate_lines_at = 120
|
||||
|
||||
[[tests.iptables]]
|
||||
name = "strips Docker chains, preserves real rules"
|
||||
input = """
|
||||
Chain INPUT (policy ACCEPT)
|
||||
num target prot opt source destination
|
||||
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
|
||||
Chain DOCKER (1 references)
|
||||
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
|
||||
Chain BR-abcdef (0 references)
|
||||
"""
|
||||
expected = "Chain INPUT (policy ACCEPT)\nnum target prot opt source destination\n1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0\n DOCKER all -- 0.0.0.0/0 0.0.0.0/0"
|
||||
|
||||
[[tests.iptables]]
|
||||
name = "preserves FORWARD and OUTPUT chains"
|
||||
input = "Chain FORWARD (policy DROP)\n1 ACCEPT tcp\nChain OUTPUT (policy ACCEPT)\n1 ACCEPT all"
|
||||
expected = "Chain FORWARD (policy DROP)\n1 ACCEPT tcp\nChain OUTPUT (policy ACCEPT)\n1 ACCEPT all"
|
||||
@@ -0,0 +1,41 @@
|
||||
[filters.make]
|
||||
description = "Compact make output"
|
||||
match_command = "^make\\b"
|
||||
strip_lines_matching = [
|
||||
"^make\\[\\d+\\]:",
|
||||
"^\\s*$",
|
||||
"^Nothing to be done",
|
||||
]
|
||||
max_lines = 50
|
||||
on_empty = "make: ok"
|
||||
|
||||
[[tests.make]]
|
||||
name = "strips entering/leaving lines"
|
||||
input = """
|
||||
make[1]: Entering directory '/home/user'
|
||||
gcc -O2 foo.c
|
||||
make[1]: Leaving directory '/home/user'
|
||||
"""
|
||||
expected = """
|
||||
gcc -O2 foo.c
|
||||
"""
|
||||
|
||||
[[tests.make]]
|
||||
name = "strips blank lines"
|
||||
input = """
|
||||
gcc -O2 foo.c
|
||||
|
||||
gcc -O2 bar.c
|
||||
"""
|
||||
expected = """
|
||||
gcc -O2 foo.c
|
||||
gcc -O2 bar.c
|
||||
"""
|
||||
|
||||
[[tests.make]]
|
||||
name = "on_empty when all stripped"
|
||||
input = """
|
||||
make[1]: Entering directory '/home/user'
|
||||
make[1]: Leaving directory '/home/user'
|
||||
"""
|
||||
expected = "make: ok"
|
||||
@@ -0,0 +1,27 @@
|
||||
[filters.mix-compile]
|
||||
description = "Compact mix compile output"
|
||||
match_command = "^mix\\s+compile(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Compiling \\d+ file",
|
||||
"^\\s*$",
|
||||
"^Generated\\s",
|
||||
]
|
||||
max_lines = 40
|
||||
on_empty = "mix compile: ok"
|
||||
|
||||
[[tests.mix-compile]]
|
||||
name = "strips compile noise, preserves warnings"
|
||||
input = """
|
||||
Compiling 12 files (.ex)
|
||||
Generated my_app app
|
||||
|
||||
warning: variable "conn" is unused
|
||||
lib/router.ex:42
|
||||
"""
|
||||
expected = "warning: variable \"conn\" is unused\n lib/router.ex:42"
|
||||
|
||||
[[tests.mix-compile]]
|
||||
name = "on_empty when only noise"
|
||||
input = "Compiling 3 files (.ex)\nGenerated my_app app\n"
|
||||
expected = "mix compile: ok"
|
||||
@@ -0,0 +1,15 @@
|
||||
[filters.mix-format]
|
||||
description = "Compact mix format output"
|
||||
match_command = "^mix\\s+format(\\s|$)"
|
||||
on_empty = "mix format: ok"
|
||||
max_lines = 20
|
||||
|
||||
[[tests.mix-format]]
|
||||
name = "empty output returns ok"
|
||||
input = ""
|
||||
expected = "mix format: ok"
|
||||
|
||||
[[tests.mix-format]]
|
||||
name = "changed files pass through"
|
||||
input = "lib/my_app.ex\ntest/my_app_test.exs"
|
||||
expected = "lib/my_app.ex\ntest/my_app_test.exs"
|
||||
@@ -0,0 +1,44 @@
|
||||
[filters.mvn-build]
|
||||
description = "Compact Maven build output"
|
||||
match_command = "^mvn\\s+(compile|package|clean|install)\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\[INFO\\] ---",
|
||||
"^\\[INFO\\] Building\\s",
|
||||
"^\\[INFO\\] Downloading\\s",
|
||||
"^\\[INFO\\] Downloaded\\s",
|
||||
"^\\[INFO\\]\\s*$",
|
||||
"^\\s*$",
|
||||
"^Downloading:",
|
||||
"^Downloaded:",
|
||||
"^Progress",
|
||||
]
|
||||
max_lines = 50
|
||||
on_empty = "mvn: ok"
|
||||
|
||||
[[tests.mvn-build]]
|
||||
name = "strips INFO noise, preserves errors and summary"
|
||||
input = """
|
||||
[INFO] ---
|
||||
[INFO] Building myapp 1.0-SNAPSHOT
|
||||
[INFO] Downloading org.apache.maven.plugins:maven-compiler-plugin:3.11.0
|
||||
[INFO] Downloaded org.apache.maven.plugins:maven-compiler-plugin:3.11.0
|
||||
[INFO]
|
||||
[ERROR] /src/main/java/Main.java:[10,5] cannot find symbol
|
||||
symbol: method foo()
|
||||
[INFO] BUILD FAILURE
|
||||
[INFO] Total time: 2.543 s
|
||||
"""
|
||||
expected = "[ERROR] /src/main/java/Main.java:[10,5] cannot find symbol\n symbol: method foo()\n[INFO] BUILD FAILURE\n[INFO] Total time: 2.543 s"
|
||||
|
||||
[[tests.mvn-build]]
|
||||
name = "successful build keeps BUILD SUCCESS line"
|
||||
input = """
|
||||
[INFO] ---
|
||||
[INFO] Building myapp 1.0-SNAPSHOT
|
||||
[INFO]
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 4.123 s
|
||||
[INFO] Finished at: 2024-01-15T10:30:00Z
|
||||
"""
|
||||
expected = "[INFO] BUILD SUCCESS\n[INFO] Total time: 4.123 s\n[INFO] Finished at: 2024-01-15T10:30:00Z"
|
||||
@@ -0,0 +1,40 @@
|
||||
[filters.pio-run]
|
||||
description = "Compact PlatformIO build output"
|
||||
match_command = "^pio\\s+run"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^Verbose mode",
|
||||
"^CONFIGURATION:",
|
||||
"^LDF:",
|
||||
"^Library Manager:",
|
||||
"^Compiling\\s",
|
||||
"^Linking\\s",
|
||||
"^Building\\s",
|
||||
"^Checking size",
|
||||
]
|
||||
max_lines = 30
|
||||
on_empty = "pio run: ok"
|
||||
|
||||
[[tests.pio-run]]
|
||||
name = "strips build noise, preserves errors"
|
||||
input = """
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32dev.html
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
Compiling .pio/build/esp32dev/src/main.cpp.o
|
||||
Building .pio/build/esp32dev/firmware.elf
|
||||
Linking .pio/build/esp32dev/firmware.elf
|
||||
Checking size .pio/build/esp32dev/firmware.elf
|
||||
src/main.cpp:10:3: error: 'LED_BUILTINN' was not declared
|
||||
"""
|
||||
expected = "src/main.cpp:10:3: error: 'LED_BUILTINN' was not declared"
|
||||
|
||||
[[tests.pio-run]]
|
||||
name = "on_empty when clean build with only noise"
|
||||
input = """
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
Compiling .pio/build/esp32dev/src/main.cpp.o
|
||||
Linking .pio/build/esp32dev/firmware.elf
|
||||
"""
|
||||
expected = "pio run: ok"
|
||||
@@ -0,0 +1,35 @@
|
||||
[filters.pre-commit]
|
||||
description = "Compact pre-commit output"
|
||||
match_command = "^pre-commit\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\[INFO\\] Installing environment",
|
||||
"^\\[INFO\\] Once installed this environment will be reused",
|
||||
"^\\[INFO\\] This may take a few minutes",
|
||||
"^\\s*$",
|
||||
]
|
||||
max_lines = 40
|
||||
|
||||
[[tests.pre-commit]]
|
||||
name = "strips INFO install noise, keeps hook results"
|
||||
input = """
|
||||
[INFO] Installing environment for https://github.com/psf/black.
|
||||
[INFO] Once installed this environment will be reused.
|
||||
[INFO] This may take a few minutes...
|
||||
Trim Trailing Whitespace.................................................Passed
|
||||
Fix End of Files.........................................................Passed
|
||||
Check Yaml...............................................................Failed
|
||||
- hook id: check-yaml
|
||||
- exit code: 1
|
||||
"""
|
||||
expected = "Trim Trailing Whitespace.................................................Passed\nFix End of Files.........................................................Passed\nCheck Yaml...............................................................Failed\n- hook id: check-yaml\n- exit code: 1"
|
||||
|
||||
[[tests.pre-commit]]
|
||||
name = "all passed — no INFO noise"
|
||||
input = """
|
||||
[INFO] Installing environment for https://github.com/pre-commit/mirrors-isort.
|
||||
[INFO] Once installed this environment will be reused.
|
||||
isort....................................................................Passed
|
||||
black....................................................................Passed
|
||||
"""
|
||||
expected = "isort....................................................................Passed\nblack....................................................................Passed"
|
||||
@@ -0,0 +1,41 @@
|
||||
[filters.quarto-render]
|
||||
description = "Compact quarto render output"
|
||||
match_command = "^quarto\\s+render"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^\\s*processing file:",
|
||||
"^\\s*\\d+/\\d+\\s",
|
||||
"^\\s*running",
|
||||
"^\\s*Rendering",
|
||||
"^pandoc ",
|
||||
"^ Validating",
|
||||
"^ Resolving",
|
||||
]
|
||||
match_output = [
|
||||
{ pattern = "Output created:", message = "ok (output created)" },
|
||||
]
|
||||
max_lines = 20
|
||||
|
||||
[[tests.quarto-render]]
|
||||
name = "success short-circuits to ok"
|
||||
input = """
|
||||
processing file: index.qmd
|
||||
Validating schema
|
||||
Resolving resources
|
||||
pandoc to html5
|
||||
Output created: _site/index.html
|
||||
"""
|
||||
expected = "ok (output created)"
|
||||
|
||||
[[tests.quarto-render]]
|
||||
name = "error passes through"
|
||||
input = """
|
||||
processing file: broken.qmd
|
||||
Validating schema
|
||||
ERROR: Render failed
|
||||
|
||||
caused by:
|
||||
syntax error at line 10
|
||||
"""
|
||||
expected = "ERROR: Render failed\ncaused by:\n syntax error at line 10"
|
||||
@@ -0,0 +1,29 @@
|
||||
[filters.shopify-theme]
|
||||
description = "Compact shopify theme push/pull output"
|
||||
match_command = "^shopify\\s+theme\\s+(push|pull)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^\\s*Uploading",
|
||||
"^\\s*Downloading",
|
||||
]
|
||||
tail_lines = 5
|
||||
max_lines = 15
|
||||
on_empty = "shopify theme: ok"
|
||||
|
||||
[[tests.shopify-theme]]
|
||||
name = "strips upload/download lines, keeps tail"
|
||||
input = """
|
||||
Uploading assets/app.css
|
||||
Uploading assets/app.js
|
||||
Uploading templates/index.liquid
|
||||
Downloading assets/old.css
|
||||
|
||||
Theme 'Development' (id: 12345) pushed to store.example.myshopify.com
|
||||
"""
|
||||
expected = "Theme 'Development' (id: 12345) pushed to store.example.myshopify.com"
|
||||
|
||||
[[tests.shopify-theme]]
|
||||
name = "on_empty when all stripped"
|
||||
input = "Uploading assets/app.css\nDownloading assets/base.css\n"
|
||||
expected = "shopify theme: ok"
|
||||
@@ -0,0 +1,16 @@
|
||||
[filters.sops]
|
||||
description = "Compact sops output"
|
||||
match_command = "^sops\\b"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
max_lines = 40
|
||||
|
||||
[[tests.sops]]
|
||||
name = "strips blank lines"
|
||||
input = "mac: xyz123\n\nversion: 3.8.1"
|
||||
expected = "mac: xyz123\nversion: 3.8.1"
|
||||
|
||||
[[tests.sops]]
|
||||
name = "preserves non-blank output unchanged"
|
||||
input = "mac: abc123\nversion: 3.8.1"
|
||||
expected = "mac: abc123\nversion: 3.8.1"
|
||||
@@ -0,0 +1,35 @@
|
||||
[filters.terraform-plan]
|
||||
description = "Compact Terraform plan output"
|
||||
match_command = "^terraform\\s+plan"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Refreshing state",
|
||||
"^\\s*#.*unchanged",
|
||||
"^\\s*$",
|
||||
"^Acquiring state lock",
|
||||
"^Releasing state lock",
|
||||
]
|
||||
max_lines = 80
|
||||
on_empty = "terraform plan: no changes detected"
|
||||
|
||||
[[tests.terraform-plan]]
|
||||
name = "strips Refreshing state lines and blank lines"
|
||||
input = """
|
||||
Acquiring state lock. This may take a few moments...
|
||||
Refreshing state... [id=vpc-abc]
|
||||
Refreshing state... [id=sg-123]
|
||||
Releasing state lock. This may take a few moments...
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# aws_instance.web will be created
|
||||
+ resource "aws_instance" "web" {}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
"""
|
||||
expected = "Terraform will perform the following actions:\n # aws_instance.web will be created\n + resource \"aws_instance\" \"web\" {}\nPlan: 1 to add, 0 to change, 0 to destroy."
|
||||
|
||||
[[tests.terraform-plan]]
|
||||
name = "strips noise, preserves non-blank content"
|
||||
input = "Refreshing state... [id=vpc-abc]\nNo changes. Your infrastructure matches the configuration."
|
||||
expected = "No changes. Your infrastructure matches the configuration."
|
||||
@@ -0,0 +1,16 @@
|
||||
[filters.tofu-fmt]
|
||||
description = "Compact OpenTofu fmt output"
|
||||
match_command = "^tofu\\s+fmt(\\s|$)"
|
||||
strip_ansi = true
|
||||
on_empty = "tofu fmt: ok (no changes)"
|
||||
max_lines = 30
|
||||
|
||||
[[tests.tofu-fmt]]
|
||||
name = "empty output returns on_empty message"
|
||||
input = ""
|
||||
expected = "tofu fmt: ok (no changes)"
|
||||
|
||||
[[tests.tofu-fmt]]
|
||||
name = "changed files pass through"
|
||||
input = "main.tf\nvariables.tf"
|
||||
expected = "main.tf\nvariables.tf"
|
||||
@@ -0,0 +1,38 @@
|
||||
[filters.tofu-init]
|
||||
description = "Compact OpenTofu init output"
|
||||
match_command = "^tofu\\s+init(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^- Downloading",
|
||||
"^- Installing",
|
||||
"^- Using previously-installed",
|
||||
"^\\s*$",
|
||||
"^Initializing provider",
|
||||
"^Initializing the backend",
|
||||
"^Initializing modules",
|
||||
]
|
||||
max_lines = 20
|
||||
on_empty = "tofu init: ok"
|
||||
|
||||
[[tests.tofu-init]]
|
||||
name = "strips downloading/installing lines"
|
||||
input = """
|
||||
Initializing the backend...
|
||||
Initializing provider plugins...
|
||||
- Downloading hashicorp/aws 5.0.0...
|
||||
- Installing hashicorp/aws 5.0.0...
|
||||
- Using previously-installed hashicorp/random 3.5.1
|
||||
|
||||
OpenTofu has been successfully initialized!
|
||||
"""
|
||||
expected = "OpenTofu has been successfully initialized!"
|
||||
|
||||
[[tests.tofu-init]]
|
||||
name = "on_empty when all noise stripped"
|
||||
input = """
|
||||
Initializing the backend...
|
||||
Initializing provider plugins...
|
||||
- Using previously-installed hashicorp/aws 5.0.0
|
||||
|
||||
"""
|
||||
expected = "tofu init: ok"
|
||||
@@ -0,0 +1,35 @@
|
||||
[filters.tofu-plan]
|
||||
description = "Compact OpenTofu plan output"
|
||||
match_command = "^tofu\\s+plan(\\s|$)"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^Refreshing state",
|
||||
"^\\s*#.*unchanged",
|
||||
"^\\s*$",
|
||||
"^Acquiring state lock",
|
||||
"^Releasing state lock",
|
||||
]
|
||||
max_lines = 80
|
||||
on_empty = "tofu plan: no changes detected"
|
||||
|
||||
[[tests.tofu-plan]]
|
||||
name = "strips Refreshing state and lock lines"
|
||||
input = """
|
||||
Acquiring state lock. This may take a few moments...
|
||||
Refreshing state... [id=vpc-abc123]
|
||||
Refreshing state... [id=sg-def456]
|
||||
Releasing state lock. This may take a few moments...
|
||||
|
||||
OpenTofu will perform the following actions:
|
||||
|
||||
# aws_instance.web will be created
|
||||
+ resource "aws_instance" "web" {}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
"""
|
||||
expected = "OpenTofu will perform the following actions:\n # aws_instance.web will be created\n + resource \"aws_instance\" \"web\" {}\nPlan: 1 to add, 0 to change, 0 to destroy."
|
||||
|
||||
[[tests.tofu-plan]]
|
||||
name = "on_empty when all noise stripped"
|
||||
input = "Refreshing state... [id=vpc-abc]\nAcquiring state lock. This may take a few moments...\nReleasing state lock. This may take a few moments..."
|
||||
expected = "tofu plan: no changes detected"
|
||||
@@ -0,0 +1,17 @@
|
||||
[filters.tofu-validate]
|
||||
description = "Compact OpenTofu validate output"
|
||||
match_command = "^tofu\\s+validate(\\s|$)"
|
||||
strip_ansi = true
|
||||
match_output = [
|
||||
{ pattern = "Success! The configuration is valid", message = "ok (valid)" },
|
||||
]
|
||||
|
||||
[[tests.tofu-validate]]
|
||||
name = "success short-circuits to ok"
|
||||
input = "Success! The configuration is valid."
|
||||
expected = "ok (valid)"
|
||||
|
||||
[[tests.tofu-validate]]
|
||||
name = "error passes through unchanged"
|
||||
input = "Error: Invalid resource type\n on main.tf line 3: resource \"aws_instancee\" \"web\""
|
||||
expected = "Error: Invalid resource type\n on main.tf line 3: resource \"aws_instancee\" \"web\""
|
||||
@@ -0,0 +1,39 @@
|
||||
[filters.trunk-build]
|
||||
description = "Compact trunk build output"
|
||||
match_command = "^trunk\\s+build"
|
||||
strip_ansi = true
|
||||
strip_lines_matching = [
|
||||
"^\\s*$",
|
||||
"^\\s*Compiling\\s",
|
||||
"^\\s*Downloading\\s",
|
||||
"^\\s*Fetching\\s",
|
||||
"^\\s*Fresh\\s",
|
||||
"^\\s*Checking\\s",
|
||||
]
|
||||
tail_lines = 10
|
||||
max_lines = 30
|
||||
on_empty = "trunk build: ok"
|
||||
|
||||
[[tests.trunk-build]]
|
||||
name = "strips compile noise, keeps tail summary"
|
||||
input = """
|
||||
Compiling tokio v1.35.0
|
||||
Compiling hyper v0.14.28
|
||||
Compiling my-crate v0.1.0
|
||||
Downloading serde v1.0.195
|
||||
Fresh regex v1.10.2
|
||||
|
||||
Finished release [optimized] target(s) in 45.23s
|
||||
Binary: target/release/my-crate (5.2MB)
|
||||
"""
|
||||
expected = " Finished release [optimized] target(s) in 45.23s\n Binary: target/release/my-crate (5.2MB)"
|
||||
|
||||
[[tests.trunk-build]]
|
||||
name = "on_empty when all noise stripped"
|
||||
input = """
|
||||
Compiling my-crate v0.1.0
|
||||
Fresh serde v1.0
|
||||
Checking tokio v1.35.0
|
||||
|
||||
"""
|
||||
expected = "trunk build: ok"
|
||||
+87
-2
@@ -12,6 +12,37 @@ const REWRITE_HOOK: &str = include_str!("../hooks/rtk-rewrite.sh");
|
||||
// Embedded slim RTK awareness instructions
|
||||
const RTK_SLIM: &str = include_str!("../hooks/rtk-awareness.md");
|
||||
|
||||
/// Template written by `rtk init` when no filters.toml exists yet.
|
||||
const FILTERS_TEMPLATE: &str = r#"# Project-local RTK filters — commit this file with your repo.
|
||||
# Filters here override user-global and built-in filters.
|
||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
||||
schema_version = 1
|
||||
|
||||
# Example: suppress build noise from a custom tool
|
||||
# [filters.my-tool]
|
||||
# description = "Compact my-tool output"
|
||||
# match_command = "^my-tool\\s+build"
|
||||
# strip_ansi = true
|
||||
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
||||
# max_lines = 30
|
||||
# on_empty = "my-tool: ok"
|
||||
"#;
|
||||
|
||||
/// Template for user-global filters (~/.config/rtk/filters.toml).
|
||||
const FILTERS_GLOBAL_TEMPLATE: &str = r#"# User-global RTK filters — apply to all your projects.
|
||||
# Project-local .rtk/filters.toml takes precedence over these.
|
||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
||||
schema_version = 1
|
||||
|
||||
# Example: suppress noise from a tool you use everywhere
|
||||
# [filters.my-global-tool]
|
||||
# description = "Compact my-global-tool output"
|
||||
# match_command = "^my-global-tool\\b"
|
||||
# strip_ansi = true
|
||||
# strip_lines_matching = ["^\\s*$"]
|
||||
# max_lines = 40
|
||||
"#;
|
||||
|
||||
/// Control flow for settings.json patching
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PatchMode {
|
||||
@@ -666,8 +697,10 @@ fn run_default_mode(_global: bool, _patch_mode: PatchMode, _verbose: u8) -> Resu
|
||||
#[cfg(unix)]
|
||||
fn run_default_mode(global: bool, patch_mode: PatchMode, verbose: u8) -> Result<()> {
|
||||
if !global {
|
||||
// Local init: unchanged behavior (full injection into ./CLAUDE.md)
|
||||
return run_claude_md_mode(false, verbose);
|
||||
// Local init: inject CLAUDE.md + generate project-local filters template
|
||||
run_claude_md_mode(false, verbose)?;
|
||||
generate_project_filters_template(verbose)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let claude_dir = resolve_claude_dir()?;
|
||||
@@ -717,11 +750,63 @@ fn run_default_mode(global: bool, patch_mode: PatchMode, verbose: u8) -> Result<
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Generate user-global filters template (~/.config/rtk/filters.toml)
|
||||
generate_global_filters_template(verbose)?;
|
||||
|
||||
println!(); // Final newline
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate .rtk/filters.toml template in the current directory if not present.
|
||||
fn generate_project_filters_template(verbose: u8) -> Result<()> {
|
||||
let rtk_dir = std::path::Path::new(".rtk");
|
||||
let path = rtk_dir.join("filters.toml");
|
||||
|
||||
if path.exists() {
|
||||
if verbose > 0 {
|
||||
eprintln!(".rtk/filters.toml already exists, skipping template");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::create_dir_all(rtk_dir)
|
||||
.with_context(|| format!("Failed to create directory: {}", rtk_dir.display()))?;
|
||||
fs::write(&path, FILTERS_TEMPLATE)
|
||||
.with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
|
||||
println!(
|
||||
" filters: {} (template, edit to add project filters)",
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate ~/.config/rtk/filters.toml template if not present.
|
||||
fn generate_global_filters_template(verbose: u8) -> Result<()> {
|
||||
let config_dir = dirs::config_dir().unwrap_or_else(|| std::path::PathBuf::from(".config"));
|
||||
let rtk_dir = config_dir.join("rtk");
|
||||
let path = rtk_dir.join("filters.toml");
|
||||
|
||||
if path.exists() {
|
||||
if verbose > 0 {
|
||||
eprintln!("{} already exists, skipping template", path.display());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::create_dir_all(&rtk_dir)
|
||||
.with_context(|| format!("Failed to create directory: {}", rtk_dir.display()))?;
|
||||
fs::write(&path, FILTERS_GLOBAL_TEMPLATE)
|
||||
.with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
|
||||
println!(
|
||||
" filters: {} (template, edit to add user-global filters)",
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hook-only mode: just the hook, no RTK.md
|
||||
#[cfg(not(unix))]
|
||||
fn run_hook_only_mode(_global: bool, _patch_mode: PatchMode, _verbose: u8) -> Result<()> {
|
||||
|
||||
+231
-14
@@ -1,12 +1,13 @@
|
||||
/// TOML-based filter DSL for RTK.
|
||||
///
|
||||
/// Provides a declarative pipeline of 8 stages that can be configured
|
||||
/// via `.rtk/filters.toml` (project-local) or built-in TOML embedded in the binary.
|
||||
/// via TOML files. Lookup priority (first match wins):
|
||||
/// 1. `.rtk/filters.toml` — project-local, committable with the repo
|
||||
/// 2. `~/.config/rtk/filters.toml` — user-global, applies to all projects
|
||||
/// 3. Built-in TOML — `src/filters/*.toml`, concatenated by build.rs and embedded at compile time
|
||||
/// 4. Passthrough — no match, handled by caller
|
||||
///
|
||||
/// Lookup priority (first match wins):
|
||||
/// 1. `.rtk/filters.toml` (PWD — project-local, committable)
|
||||
/// 2. Built-in TOML (`src/builtin_filters.toml`, embedded at compile time)
|
||||
/// 3. Passthrough (no match — handled by caller)
|
||||
/// `rtk init` generates a commented template for both levels (project or global).
|
||||
///
|
||||
/// Environment variables:
|
||||
/// - `RTK_NO_TOML=1` — bypass TOML engine entirely
|
||||
@@ -26,6 +27,9 @@ use regex::{Regex, RegexSet};
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Built-in filters: concatenated from src/filters/*.toml by build.rs at compile time.
|
||||
const BUILTIN_TOML: &str = include_str!(concat!(env!("OUT_DIR"), "/builtin_filters.toml"));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deserialization types (TOML schema)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -179,8 +183,19 @@ impl TomlFilterRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: built-in (embedded at compile time)
|
||||
let builtin = include_str!("builtin_filters.toml");
|
||||
// Priority 2: user-global ~/.config/rtk/filters.toml
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
let global_path = config_dir.join("rtk").join("filters.toml");
|
||||
if let Ok(content) = std::fs::read_to_string(&global_path) {
|
||||
match Self::parse_and_compile(&content, "user-global") {
|
||||
Ok(f) => filters.extend(f),
|
||||
Err(e) => eprintln!("[rtk] warning: {}: {}", global_path.display(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: built-in (embedded at compile time)
|
||||
let builtin = BUILTIN_TOML;
|
||||
match Self::parse_and_compile(builtin, "builtin") {
|
||||
Ok(f) => filters.extend(f),
|
||||
Err(e) => eprintln!("[rtk] warning: builtin filters: {}", e),
|
||||
@@ -211,6 +226,61 @@ impl TomlFilterRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands already handled by dedicated Rust modules (routed by Clap before TOML).
|
||||
/// A TOML filter whose match_command matches one of these will never activate —
|
||||
/// Clap routes the command before `run_fallback()` is reached.
|
||||
const RUST_HANDLED_COMMANDS: &[&str] = &[
|
||||
"ls",
|
||||
"tree",
|
||||
"read",
|
||||
"smart",
|
||||
"git",
|
||||
"gh",
|
||||
"aws",
|
||||
"psql",
|
||||
"pnpm",
|
||||
"err",
|
||||
"test",
|
||||
"json",
|
||||
"deps",
|
||||
"env",
|
||||
"find",
|
||||
"diff",
|
||||
"log",
|
||||
"docker",
|
||||
"kubectl",
|
||||
"summary",
|
||||
"grep",
|
||||
"init",
|
||||
"wget",
|
||||
"wc",
|
||||
"gain",
|
||||
"config",
|
||||
"vitest",
|
||||
"prisma",
|
||||
"tsc",
|
||||
"next",
|
||||
"lint",
|
||||
"prettier",
|
||||
"format",
|
||||
"playwright",
|
||||
"cargo",
|
||||
"npm",
|
||||
"npx",
|
||||
"curl",
|
||||
"discover",
|
||||
"ruff",
|
||||
"pytest",
|
||||
"mypy",
|
||||
"pip",
|
||||
"go",
|
||||
"golangci-lint",
|
||||
"rewrite",
|
||||
"proxy",
|
||||
"verify",
|
||||
"learn",
|
||||
];
|
||||
|
||||
fn compile_filter(name: String, def: TomlFilterDef) -> Result<CompiledFilter, String> {
|
||||
// Mutual exclusion: strip and keep cannot both be set
|
||||
if !def.strip_lines_matching.is_empty() && !def.keep_lines_matching.is_empty() {
|
||||
@@ -220,6 +290,19 @@ fn compile_filter(name: String, def: TomlFilterDef) -> Result<CompiledFilter, St
|
||||
let match_regex = Regex::new(&def.match_command)
|
||||
.map_err(|e| format!("invalid match_command regex: {}", e))?;
|
||||
|
||||
// Shadow warning: if match_command matches a Rust-handled command, this filter
|
||||
// will never activate (Clap routes before run_fallback). Warn the author.
|
||||
for cmd in RUST_HANDLED_COMMANDS {
|
||||
if match_regex.is_match(cmd) {
|
||||
eprintln!(
|
||||
"[rtk] warning: filter '{}' match_command matches '{}' which is already \
|
||||
handled by a Rust module — this filter will never activate for that command",
|
||||
name, cmd
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let replace = def
|
||||
.replace
|
||||
.into_iter()
|
||||
@@ -416,7 +499,7 @@ pub fn run_filter_tests(filter_name_opt: Option<&str>) -> VerifyResults {
|
||||
let mut tested_filter_names: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
|
||||
let builtin = include_str!("builtin_filters.toml");
|
||||
let builtin = BUILTIN_TOML;
|
||||
collect_test_outcomes(
|
||||
builtin,
|
||||
filter_name_opt,
|
||||
@@ -819,12 +902,12 @@ match_command = "^cmd"
|
||||
|
||||
#[test]
|
||||
fn test_builtin_filters_compile() {
|
||||
// Compile-time safety: panics if builtin_filters.toml is broken
|
||||
let builtin = include_str!("builtin_filters.toml");
|
||||
// Compile-time safety: panics if any src/filters/*.toml is broken
|
||||
let builtin = BUILTIN_TOML;
|
||||
let result = TomlFilterRegistry::parse_and_compile(builtin, "builtin");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"builtin_filters.toml failed to compile: {:?}",
|
||||
"builtin filters failed to compile: {:?}",
|
||||
result
|
||||
);
|
||||
assert!(!result.unwrap().is_empty());
|
||||
@@ -869,7 +952,7 @@ match_command = "^make\\b"
|
||||
max_lines = 999
|
||||
"#,
|
||||
);
|
||||
let builtin = make_filters(include_str!("builtin_filters.toml"));
|
||||
let builtin = make_filters(BUILTIN_TOML);
|
||||
|
||||
// Simulate the registry: project first
|
||||
let mut all = project;
|
||||
@@ -885,7 +968,7 @@ max_lines = 999
|
||||
|
||||
#[test]
|
||||
fn test_terraform_savings_above_60pct() {
|
||||
let filters = make_filters(include_str!("builtin_filters.toml"));
|
||||
let filters = make_filters(BUILTIN_TOML);
|
||||
let filter = find_filter_in("terraform plan", &filters).expect("terraform-plan built-in");
|
||||
|
||||
// Inline fixture: realistic terraform plan with many Refreshing state lines (noise).
|
||||
@@ -949,7 +1032,7 @@ max_lines = 999
|
||||
|
||||
#[test]
|
||||
fn test_make_savings_above_60pct() {
|
||||
let filters = make_filters(include_str!("builtin_filters.toml"));
|
||||
let filters = make_filters(BUILTIN_TOML);
|
||||
let filter = find_filter_in("make all", &filters).expect("make built-in");
|
||||
|
||||
let input = r#"make[1]: Entering directory '/home/user/project'
|
||||
@@ -1302,4 +1385,138 @@ match_command = "^make\\b"
|
||||
assert!(all_names.contains(&"make".to_string()));
|
||||
assert!(!tested.contains("make"));
|
||||
}
|
||||
|
||||
// --- Multi-file architecture tests (build.rs) ---
|
||||
|
||||
/// Verify BUILTIN_TOML was generated with the correct schema_version header.
|
||||
/// build.rs injects it — if the const is somehow stale this fails immediately.
|
||||
#[test]
|
||||
fn test_builtin_toml_has_schema_version() {
|
||||
assert!(
|
||||
BUILTIN_TOML.contains("schema_version = 1"),
|
||||
"BUILTIN_TOML must start with 'schema_version = 1' (injected by build.rs)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify every expected filter name is present in BUILTIN_TOML.
|
||||
/// This is the safeguard against accidentally deleting a filter file.
|
||||
#[test]
|
||||
fn test_builtin_all_expected_filters_present() {
|
||||
let filters = make_filters(BUILTIN_TOML);
|
||||
let names: std::collections::HashSet<&str> =
|
||||
filters.iter().map(|f| f.name.as_str()).collect();
|
||||
|
||||
let expected = [
|
||||
"ansible-playbook",
|
||||
"du",
|
||||
"fail2ban-client",
|
||||
"gcloud",
|
||||
"helm",
|
||||
"iptables",
|
||||
"make",
|
||||
"mix-compile",
|
||||
"mix-format",
|
||||
"mvn-build",
|
||||
"pio-run",
|
||||
"pre-commit",
|
||||
"quarto-render",
|
||||
"shopify-theme",
|
||||
"sops",
|
||||
"terraform-plan",
|
||||
"tofu-fmt",
|
||||
"tofu-init",
|
||||
"tofu-plan",
|
||||
"tofu-validate",
|
||||
"trunk-build",
|
||||
];
|
||||
|
||||
for name in &expected {
|
||||
assert!(
|
||||
names.contains(name),
|
||||
"Built-in filter '{}' is missing — was its .toml file deleted from src/filters/?",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the exact count of built-in filters.
|
||||
/// Fails if a file is added/removed without updating this test.
|
||||
#[test]
|
||||
fn test_builtin_filter_count() {
|
||||
let filters = make_filters(BUILTIN_TOML);
|
||||
assert_eq!(
|
||||
filters.len(),
|
||||
21,
|
||||
"Expected exactly 21 built-in filters, got {}. \
|
||||
Update this count when adding/removing filters in src/filters/.",
|
||||
filters.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that every built-in filter has at least one inline test.
|
||||
/// Prevents shipping filters with zero test coverage.
|
||||
#[test]
|
||||
fn test_builtin_all_filters_have_inline_tests() {
|
||||
let mut all_names: Vec<String> = Vec::new();
|
||||
let mut tested: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut outcomes = Vec::new();
|
||||
collect_test_outcomes(
|
||||
BUILTIN_TOML,
|
||||
None,
|
||||
&mut outcomes,
|
||||
&mut all_names,
|
||||
&mut tested,
|
||||
);
|
||||
|
||||
let untested: Vec<&str> = all_names
|
||||
.iter()
|
||||
.filter(|name| !tested.contains(name.as_str()))
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
untested.is_empty(),
|
||||
"The following built-in filters have no inline tests: {:?}\n\
|
||||
Add [[tests.<name>]] entries to the corresponding src/filters/<name>.toml file.",
|
||||
untested
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that adding a new filter entry to any TOML content makes it
|
||||
/// immediately discoverable via find_filter_in — simulating how a new
|
||||
/// src/filters/my-tool.toml would work after cargo build.
|
||||
#[test]
|
||||
fn test_new_filter_discoverable_after_concat() {
|
||||
// Simulate build.rs: concat BUILTIN_TOML with a brand-new filter block
|
||||
let new_filter = r#"
|
||||
[filters.my-new-tool]
|
||||
description = "Compact my-new-tool output"
|
||||
match_command = "^my-new-tool\\b"
|
||||
strip_lines_matching = ["^\\s*$"]
|
||||
max_lines = 30
|
||||
on_empty = "my-new-tool: ok"
|
||||
|
||||
[[tests.my-new-tool]]
|
||||
name = "strips blank lines"
|
||||
input = "output line 1\n\noutput line 2"
|
||||
expected = "output line 1\noutput line 2"
|
||||
"#;
|
||||
let combined = format!("{}\n\n{}", BUILTIN_TOML, new_filter);
|
||||
let filters = make_filters(&combined);
|
||||
|
||||
// All 21 existing filters still present + 1 new = 22
|
||||
assert_eq!(
|
||||
filters.len(),
|
||||
22,
|
||||
"Expected 22 filters after concat (21 built-in + 1 new)"
|
||||
);
|
||||
|
||||
// New filter is discoverable
|
||||
let found = find_filter_in("my-new-tool --verbose", &filters);
|
||||
assert!(
|
||||
found.is_some(),
|
||||
"Newly added filter must be discoverable via find_filter_in"
|
||||
);
|
||||
assert_eq!(found.unwrap().name, "my-new-tool");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user