Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 8 additions & 17 deletions src/cmds/dotnet/dotnet_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Filters dotnet CLI output — build, test, and format results.

use crate::binlog;
use crate::core::runner;
use crate::core::stream::exec_capture;
use crate::core::tracking;
use crate::core::utils::{resolved_command, truncate};
Expand Down Expand Up @@ -76,7 +77,6 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
anyhow::bail!("dotnet: no subcommand specified");
}

let timer = tracking::TimedExecution::start();
let subcommand = args[0].to_string_lossy().to_string();

let mut cmd = resolved_command("dotnet");
Expand All @@ -90,22 +90,13 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
eprintln!("Running: dotnet {} ...", subcommand);
}

let result =
exec_capture(&mut cmd).with_context(|| format!("Failed to run dotnet {}", subcommand))?;

let raw = format!("{}\n{}", result.stdout, result.stderr);

print!("{}", result.stdout);
eprint!("{}", result.stderr);

timer.track(
&format!("dotnet {}", subcommand),
&format!("rtk dotnet {}", subcommand),
&raw,
&raw,
);

Ok(result.exit_code)
runner::run(
cmd,
"dotnet",
&tracking::args_display(args),
runner::RunMode::Passthrough,
runner::RunOptions::default(),
)
}

fn run_dotnet_with_binlog(subcommand: &str, args: &[String], verbose: u8) -> Result<i32> {
Expand Down
8 changes: 7 additions & 1 deletion src/cmds/git/gh_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,16 @@ fn pr_checks(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<i32>
for arg in &extra_args {
cmd.arg(arg);
}
let mut args_display = format!("pr checks {}", pr_number);
if !extra_args.is_empty() {
args_display.push(' ');
args_display.push_str(&extra_args.join(" "));
}

runner::run_filtered(
cmd,
"gh",
&format!("pr checks {}", pr_number),
&args_display,
format_pr_checks,
RunOptions::stdout_only()
.early_exit_on_failure()
Expand Down
28 changes: 7 additions & 21 deletions src/cmds/go/go_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ pub fn run_other(args: &[OsString], verbose: u8) -> Result<i32> {
}
}

let timer = tracking::TimedExecution::start();

let subcommand = args[0].to_string_lossy();
let mut cmd = resolved_command("go");
cmd.arg(&*subcommand);
Expand All @@ -147,25 +145,13 @@ pub fn run_other(args: &[OsString], verbose: u8) -> Result<i32> {
eprintln!("Running: go {} ...", subcommand);
}

let output = cmd
.output()
.with_context(|| format!("Failed to run go {}", subcommand))?;

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let raw = format!("{}\n{}", stdout, stderr);

print!("{}", stdout);
eprint!("{}", stderr);

timer.track(
&format!("go {}", subcommand),
&format!("rtk go {}", subcommand),
&raw,
&raw, // No filtering for unsupported commands
);

Ok(exit_code_from_output(&output, "go"))
runner::run(
cmd,
"go",
&tracking::args_display(args),
runner::RunMode::Passthrough,
runner::RunOptions::default(),
)
}

/// Detect golangci-lint major version when invoked via `go tool`.
Expand Down
11 changes: 11 additions & 0 deletions src/cmds/js/playwright_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Filters Playwright E2E test output to show only failures.

use crate::core::runner;
use crate::core::stream::exec_capture;
use crate::core::tracking;
use crate::core::utils::{detect_package_manager, resolved_command, strip_ansi};
Expand Down Expand Up @@ -286,6 +287,16 @@ pub fn run(args: &[String], verbose: u8) -> Result<i32> {
eprintln!("Running: playwright {}", args.join(" "));
}

if args.iter().any(|arg| matches!(arg.as_str(), "codegen" | "show-report" | "--ui")) {
return runner::run(
cmd,
"playwright",
&args.join(" "),
runner::RunMode::Passthrough,
runner::RunOptions::default(),
);
}

let result = exec_capture(&mut cmd)
.context("Failed to run playwright (try: npm install -g playwright)")?;

Expand Down
89 changes: 89 additions & 0 deletions src/core/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,41 @@ pub enum RunMode<'a> {
Passthrough,
}

/// Detect long-running invocations before `RunMode::Filtered` captures output.
///
/// `args_display` must be structured command arguments, not user-controlled prose.
/// `RunMode::Streamed` and `RunMode::Passthrough` do not call this helper.
fn is_streaming_invocation(tool_name: &str, args_display: &str) -> bool {
let tool = tool_name.split_whitespace().next().unwrap_or(tool_name);
let args: Vec<&str> = args_display.split_whitespace().collect();

if args
.iter()
.any(|arg| matches!(*arg, "--watch" | "--watchAll"))
{
return true;
}

if args.contains(&"-w") && matches!(tool, "jest" | "vitest" | "webpack" | "nodemon") {
return true;
}

let follows = args.iter().any(|arg| matches!(*arg, "--follow" | "-f"));
if follows && matches!(tool, "docker" | "kubectl" | "tail" | "journalctl") {
return true;
}

(tool == "go" && args.contains(&"run"))
|| (tool == "playwright" && args.contains(&"codegen"))
|| matches!(tool, "tail" | "nodemon" | "watchman")
|| args.iter().any(|arg| {
matches!(
*arg,
"watch" | "dev" | "serve" | "start" | "codegen" | "show-report"
)
})
}

pub fn run(
mut cmd: Command,
tool_name: &str,
Expand All @@ -71,6 +106,15 @@ pub fn run(

match mode {
RunMode::Filtered(filter_fn) => {
if is_streaming_invocation(tool_name, args_display) {
let result =
stream::run_streaming(&mut cmd, StdinMode::Inherit, FilterMode::Passthrough)
.with_context(|| format!("Failed to run {}", tool_name))?;

timer.track_passthrough(&cmd_label, &format!("rtk {} (streaming)", cmd_label));
return Ok(result.exit_code);
}

let result = stream::run_streaming(&mut cmd, StdinMode::Null, FilterMode::CaptureOnly)
.with_context(|| format!("Failed to run {}", tool_name))?;

Expand Down Expand Up @@ -199,3 +243,48 @@ pub fn run_streamed(
opts,
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_streaming_invocation_detects_watch_flags() {
assert!(is_streaming_invocation("prettier", "--watch ."));
assert!(is_streaming_invocation("gh", "pr checks 123 --watch"));
assert!(is_streaming_invocation("jest", "run -w"));
}

#[test]
fn test_is_streaming_invocation_scopes_short_watch_flag() {
assert!(is_streaming_invocation("vitest", "run -w"));
assert!(!is_streaming_invocation("wc", "-w src/main.rs"));
assert!(!is_streaming_invocation("cargo", "test -w"));
}

#[test]
fn test_is_streaming_invocation_detects_follow_flags_for_log_tools() {
assert!(is_streaming_invocation("docker", "logs -f web"));
assert!(is_streaming_invocation("kubectl", "logs --follow web"));
assert!(!is_streaming_invocation("git", "log -f"));
}

#[test]
fn test_is_streaming_invocation_detects_dev_and_watch_subcommands() {
assert!(is_streaming_invocation("npm", "run dev"));
assert!(is_streaming_invocation("pnpm", "run dev"));
assert!(is_streaming_invocation("npm", "start"));
assert!(is_streaming_invocation("playwright", "codegen"));
assert!(is_streaming_invocation("dotnet", "watch run"));
assert!(is_streaming_invocation("go", "run ."));
}

#[test]
fn test_is_streaming_invocation_keeps_finite_commands_filterable() {
assert!(!is_streaming_invocation("npm", "run build"));
assert!(!is_streaming_invocation("gh", "pr checks 123"));
assert!(!is_streaming_invocation("go", "test ./..."));
assert!(!is_streaming_invocation("go test", "./..."));
assert!(!is_streaming_invocation("dotnet", "build"));
}
}
1 change: 1 addition & 0 deletions src/core/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub struct RegexBlockFilter {

#[cfg(test)]
impl RegexBlockFilter {
#[allow(dead_code)] // public helper used by tests and available for future filters
pub fn new(tool_name: &str, start_pattern: &str) -> Self {
Self {
start_re: Regex::new(start_pattern).unwrap_or_else(|e| {
Expand Down