diff --git a/CHANGELOG.md b/CHANGELOG.md index e98489a2..56069a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes +* **npx:** route unknown tools through npx instead of npm ([#815](https://github.com/rtk-ai/rtk/issues/815)) * **hook:** respect Claude Code deny/ask permission rules on rewrite — hook now checks settings.json before rewriting commands, preventing bypass of user-configured deny/ask permissions * **git:** replace symbol prefixes (`* branch`, `+ Staged:`, `~ Modified:`, `? Untracked:`) with plain lowercase labels (`branch:`, `staged:`, `modified:`, `untracked:`) in git status output * **ruby:** use `rails test` instead of `rake test` when positional file args are passed — `rake test` ignores positional files and only supports `TEST=path` diff --git a/src/cmds/js/npm_cmd.rs b/src/cmds/js/npm_cmd.rs index 7c86fe77..995d227b 100644 --- a/src/cmds/js/npm_cmd.rs +++ b/src/cmds/js/npm_cmd.rs @@ -134,7 +134,7 @@ pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> { } /// Filter npm run output - strip boilerplate, progress bars, npm WARN -fn filter_npm_output(output: &str) -> String { +pub fn filter_npm_output(output: &str) -> String { let mut result = Vec::new(); for line in output.lines() { diff --git a/src/main.rs b/src/main.rs index 50a39ce5..1e7c62bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1972,8 +1972,31 @@ fn main() -> Result<()> { playwright_cmd::run(&args[1..], cli.verbose)?; } _ => { - // Generic passthrough with npm boilerplate filter - npm_cmd::run(&args, cli.verbose, cli.skip_env)?; + let timer = core::tracking::TimedExecution::start(); + let mut cmd = core::utils::resolved_command("npx"); + cmd.args(&args); + if cli.skip_env { + cmd.env("SKIP_ENV_VALIDATION", "1"); + } + if cli.verbose > 0 { + eprintln!("Running: npx {}", args.join(" ")); + } + let output = cmd.output().context("Failed to run npx")?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let raw = format!("{}\n{}", stdout, stderr); + let filtered = npm_cmd::filter_npm_output(&raw); + println!("{}", filtered); + let args_str = args.join(" "); + timer.track( + &format!("npx {}", args_str), + &format!("rtk npx {}", args_str), + &raw, + &filtered, + ); + if !output.status.success() { + std::process::exit(output.status.code().unwrap_or(1)); + } } } } @@ -2598,4 +2621,39 @@ mod tests { } } } + + #[test] + fn test_npx_parses_simple_command() { + let cli = Cli::try_parse_from(["rtk", "npx", "cowsay", "hello"]).unwrap(); + match cli.command { + Commands::Npx { args } => { + assert_eq!(args, vec!["cowsay", "hello"]); + } + _ => panic!("Expected Npx command"), + } + } + + #[test] + fn test_npx_parses_flags_before_command() { + let cli = Cli::try_parse_from(["rtk", "npx", "--no-install", "cowsay", "hello"]).unwrap(); + match cli.command { + Commands::Npx { args } => { + assert_eq!(args, vec!["--no-install", "cowsay", "hello"]); + } + _ => panic!("Expected Npx command"), + } + } + + #[test] + fn test_npx_parses_known_tool_names() { + for tool in &["tsc", "eslint", "prisma", "next", "prettier", "playwright"] { + let cli = Cli::try_parse_from(["rtk", "npx", tool, "--help"]).unwrap(); + match cli.command { + Commands::Npx { ref args } => { + assert_eq!(args[0], *tool); + } + _ => panic!("Expected Npx command for {}", tool), + } + } + } }