Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ RTK intercepts shell commands and compresses their output before your agent read
| `git add/commit/push` | Confirmation line instead of full progress output |
| `cargo test` / `npm test` | Failures only, passing tests collapsed to a count |
| `ruff check` | Grouped by rule and file |
| `sqlfluff lint` | Grouped by rule and file |
| `pytest` | Failures only, traceback trimmed |
| `go test` | NDJSON parsed, failures only |
| `docker ps` | Essential fields only |
Expand Down Expand Up @@ -205,6 +206,8 @@ rtk test <cmd> # Generic test wrapper - failures only (-90%)
```bash
rtk lint # ESLint grouped by rule/file
rtk lint biome # Supports other linters
rtk sqlfluff lint # SQL linting (JSON, -75%)
rtk sqlfluff lint models/ # Lint a specific directory (pass path after `lint`)
rtk tsc # TypeScript errors grouped by file
rtk next build # Next.js build compact
rtk prettier --check . # Files needing formatting
Expand Down
60 changes: 39 additions & 21 deletions src/cmds/js/lint_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::core::stream::exec_capture;
use crate::core::tracking;
use crate::core::truncate::{CAP_ERRORS, CAP_WARNINGS};
use crate::core::utils::{resolved_command, tool_exec, truncate, MissingTool};
use crate::cmds::python::sqlfluff_cmd;
use crate::mypy_cmd;
use crate::ruff_cmd;
use anyhow::{Context, Result};
Expand Down Expand Up @@ -54,7 +55,7 @@ struct PylintDiagnostic {

/// Check if a linter is Python-based (uses pip/pipx, not npm/pnpm)
fn is_python_linter(linter: &str) -> bool {
matches!(linter, "ruff" | "pylint" | "mypy" | "flake8")
matches!(linter, "ruff" | "pylint" | "mypy" | "flake8" | "sqlfluff")
}

/// Strip package manager prefixes (npx, bunx, pnpm, pnpm exec, yarn) from args.
Expand Down Expand Up @@ -106,7 +107,12 @@ pub fn run(runner: Option<&str>, args: &[String], verbose: u8) -> Result<i32> {

let (linter, explicit) = detect_linter(effective_args);

// Python linters use resolved_command() directly (they're on PATH via pip/pipx)
// sqlfluff owns its own argv and its own rendering: routing, format-flag
// detection and failure handling live in `sqlfluff_cmd::plan` so this entry
// point and `rtk sqlfluff ...` cannot drift apart.
let sqlfluff = (linter == "sqlfluff").then(|| sqlfluff_cmd::plan(&effective_args[1..]));

// Python linter use resolved_command() directly (they're on PATH via pip/pipx)
// JS linters use package_manager_exec (npx/pnpm exec)
let mut cmd = if is_python_linter(linter) {
resolved_command(linter)
Expand All @@ -127,6 +133,8 @@ pub fn run(runner: Option<&str>, args: &[String], verbose: u8) -> Result<i32> {
"pylint" if !effective_args.contains(&"--output-format".to_string()) => {
cmd.arg("--output-format=json2");
}
// sqlfluff's full argv comes from the plan below, flags included.
"sqlfluff" => {}
"mypy" => {
// mypy uses default text output (no special flags)
}
Expand All @@ -149,15 +157,20 @@ pub fn run(runner: Option<&str>, args: &[String], verbose: u8) -> Result<i32> {
1
};

for arg in &effective_args[start_idx..] {
// Skip --output-format if we already added it
if linter == "ruff" && arg.starts_with("--output-format") {
continue;
}
if linter == "pylint" && arg.starts_with("--output-format") {
continue;
if let Some(plan) = &sqlfluff {
// sqlfluff's argv is planned whole, subcommand and format flags included.
cmd.args(&plan.args);
} else {
for arg in &effective_args[start_idx..] {
// Skip --output-format if we already added it
if linter == "ruff" && arg.starts_with("--output-format") {
continue;
}
if linter == "pylint" && arg.starts_with("--output-format") {
continue;
}
cmd.arg(arg);
}
cmd.arg(arg);
}

// Default to current directory if no path specified (for ruff/pylint/mypy/eslint)
Expand Down Expand Up @@ -195,19 +208,23 @@ pub fn run(runner: Option<&str>, args: &[String], verbose: u8) -> Result<i32> {
let raw = format!("{}\n{}", result.stdout, result.stderr);

// Dispatch to appropriate filter based on linter
let filtered = match linter {
"eslint" => filter_eslint_json(&result.stdout),
"ruff" => {
// Reuse ruff_cmd's JSON parser
if !result.stdout.trim().is_empty() {
ruff_cmd::filter_ruff_check_json(&result.stdout)
} else {
"Ruff: No issues found".to_string()
let filtered = if let Some(plan) = &sqlfluff {
plan.render(&result.stdout, result.exit_code)
} else {
match linter {
"eslint" => filter_eslint_json(&result.stdout),
"ruff" => {
// Reuse ruff_cmd's JSON parser
if !result.stdout.trim().is_empty() {
ruff_cmd::filter_ruff_check_json(&result.stdout)
} else {
"Ruff: No issues found".to_string()
}
}
"pylint" => filter_pylint_json(&result.stdout),
"mypy" => mypy_cmd::filter_mypy_output(&raw),
_ => filter_generic_lint(&raw),
}
"pylint" => filter_pylint_json(&result.stdout),
"mypy" => mypy_cmd::filter_mypy_output(&raw),
_ => filter_generic_lint(&raw),
};

let hint = crate::core::tee::tee_and_hint(&raw, "lint", result.exit_code);
Expand Down Expand Up @@ -716,6 +733,7 @@ mod tests {
assert!(is_python_linter("pylint"));
assert!(is_python_linter("mypy"));
assert!(is_python_linter("flake8"));
assert!(is_python_linter("sqlfluff"));
assert!(!is_python_linter("eslint"));
assert!(!is_python_linter("biome"));
assert!(!is_python_linter("unknown"));
Expand Down
1 change: 1 addition & 0 deletions src/cmds/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `pytest_cmd.rs` uses a state machine text parser (no JSON available from pytest)
- `ruff_cmd.rs` uses JSON for check mode (`--output-format=json`) and text filtering for format mode
- `sqlfluff_cmd.rs` injects `--format json` for lint mode and groups violations by rule and file; other subcommands pass through capped
- `pip_cmd.rs` auto-detects `uv` as a pip alternative and routes accordingly
- `uv_cmd.rs` preserves `uv run` environment semantics: the program's own output passes through on success (bounded, with a tee hint), diagnostics are extracted on failure
- `python -m pytest` and `python3 -m mypy` are rewritten by the hook registry to `rtk pytest` / `rtk mypy`
Expand Down
Loading
Loading