From e4e0eb2c3bd24e3379216000b201a42fc662564f Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Wed, 4 Mar 2026 14:57:23 +0100 Subject: [PATCH 1/8] fix: resolve Windows PATHEXT for .CMD/.BAT/.PS1 tool wrappers (#212) Rust's std::process::Command::new() does not honor Windows PATHEXT, so Node.js tools installed as .CMD/.BAT shims (vitest, eslint, tsc, pnpm, etc.) fail with "program not found" on Windows. - Add `which` crate for proper PATH+PATHEXT binary resolution - Add resolve_binary(), resolved_command(), tool_exists() to utils.rs - Replace Command::new() with resolved_command() across 22 modules - Remove duplicate which_command() helpers from pip/pytest/ccusage - Add troubleshooting guide for Windows "program not found" errors - Add 213 lines of tests including Windows .CMD/.BAT wrapper tests Closes #212 --- Cargo.lock | 37 +++-- Cargo.toml | 1 + docs/TROUBLESHOOTING.md | 28 ++++ src/ccusage.rs | 13 +- src/container.rs | 32 ++--- src/curl_cmd.rs | 5 +- src/format_cmd.rs | 7 +- src/golangci_cmd.rs | 5 +- src/grep_cmd.rs | 10 +- src/lint_cmd.rs | 8 +- src/ls.rs | 4 +- src/main.rs | 12 +- src/mypy_cmd.rs | 20 +-- src/next_cmd.rs | 13 +- src/npm_cmd.rs | 4 +- src/pip_cmd.rs | 22 +-- src/playwright_cmd.rs | 8 +- src/pnpm_cmd.rs | 10 +- src/prisma_cmd.rs | 13 +- src/pytest_cmd.rs | 21 +-- src/ruff_cmd.rs | 5 +- src/tree.rs | 7 +- src/tsc_cmd.rs | 12 +- src/utils.rs | 292 +++++++++++++++++++++++++++++++++++++--- src/wc_cmd.rs | 4 +- src/wget_cmd.rs | 6 +- 26 files changed, 409 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94ad86c2..d2a2a829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,15 +313,10 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "env_home" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "equivalent" @@ -858,6 +853,7 @@ dependencies = [ "toml", "ureq", "walkdir", + "which", ] [[package]] @@ -1284,21 +1280,14 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" +name = "which" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "rustls-pki-types", + "env_home", + "rustix", + "winsafe", ] [[package]] @@ -1535,6 +1524,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 0cf7de80..bdbf6caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ tempfile = "3" sha2 = "0.10" ureq = "2" hostname = "0.4" +which = "8" [dev-dependencies] diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 64d45763..dc98d610 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -167,6 +167,34 @@ Then add to `~/.claude/settings.json` (replace `~` with full path): --- +## Problem: RTK commands fail on Windows ("program not found" or "No such file") + +### Symptom +``` +rtk vitest --run +# Error: program not found +# Or: The system cannot find the file specified + +rtk lint . +# Error: No such file or directory +``` + +### Root Cause +On Windows, Node.js tools (vitest, eslint, tsc, etc.) are installed as `.CMD` or `.BAT` wrapper scripts, not as native `.exe` binaries. Rust's `std::process::Command::new("vitest")` does not honor the Windows `PATHEXT` environment variable, so it cannot find `vitest.CMD` even when it's on PATH. + +### Solution +Update to rtk v0.23.1+ which resolves this via the `which` crate for proper PATH+PATHEXT resolution. All 16+ command modules now use `resolved_command()` instead of `Command::new()`. + +```bash +cargo install --git https://github.com/rtk-ai/rtk +rtk --version # Should be 0.23.1+ +``` + +### Affected Commands +All commands that spawn external tools: `rtk vitest`, `rtk lint`, `rtk tsc`, `rtk pnpm`, `rtk playwright`, `rtk prisma`, `rtk next`, `rtk prettier`, `rtk ruff`, `rtk pytest`, `rtk pip`, `rtk mypy`, `rtk golangci-lint`, and others. + +--- + ## Problem: "command not found: rtk" after installation ### Symptom diff --git a/src/ccusage.rs b/src/ccusage.rs index 822cca15..99e88c7f 100644 --- a/src/ccusage.rs +++ b/src/ccusage.rs @@ -4,6 +4,7 @@ //! Claude Code API usage metrics. Handles subprocess execution, JSON parsing, //! and graceful degradation when ccusage is unavailable. +use crate::utils::{resolved_command, tool_exists}; use anyhow::{Context, Result}; use serde::Deserialize; use std::process::Command; @@ -84,21 +85,17 @@ struct MonthlyEntry { /// Check if ccusage binary exists in PATH fn binary_exists() -> bool { - Command::new("which") - .arg("ccusage") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + tool_exists("ccusage") } /// Build the ccusage command, falling back to npx if binary not in PATH fn build_command() -> Option { if binary_exists() { - return Some(Command::new("ccusage")); + return Some(resolved_command("ccusage")); } // Fallback: try npx - let npx_check = Command::new("npx") + let npx_check = resolved_command("npx") .arg("ccusage") .arg("--help") .stdout(std::process::Stdio::null()) @@ -106,7 +103,7 @@ fn build_command() -> Option { .status(); if npx_check.map(|s| s.success()).unwrap_or(false) { - let mut cmd = Command::new("npx"); + let mut cmd = resolved_command("npx"); cmd.arg("ccusage"); return Some(cmd); } diff --git a/src/container.rs b/src/container.rs index 759154ba..f0f0f309 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,7 +1,7 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; use std::ffi::OsString; -use std::process::Command; #[derive(Debug, Clone, Copy)] pub enum ContainerCmd { @@ -27,13 +27,13 @@ pub fn run(cmd: ContainerCmd, args: &[String], verbose: u8) -> Result<()> { fn docker_ps(_verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let raw = Command::new("docker") + let raw = resolved_command("docker") .args(["ps"]) .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .unwrap_or_default(); - let output = Command::new("docker") + let output = resolved_command("docker") .args([ "ps", "--format", @@ -84,13 +84,13 @@ fn docker_ps(_verbose: u8) -> Result<()> { fn docker_images(_verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let raw = Command::new("docker") + let raw = resolved_command("docker") .args(["images"]) .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .unwrap_or_default(); - let output = Command::new("docker") + let output = resolved_command("docker") .args(["images", "--format", "{{.Repository}}:{{.Tag}}\t{{.Size}}"]) .output() .context("Failed to run docker images")?; @@ -160,7 +160,7 @@ fn docker_logs(args: &[String], _verbose: u8) -> Result<()> { return Ok(()); } - let output = Command::new("docker") + let output = resolved_command("docker") .args(["logs", "--tail", "100", container]) .output() .context("Failed to run docker logs")?; @@ -184,7 +184,7 @@ fn docker_logs(args: &[String], _verbose: u8) -> Result<()> { fn kubectl_pods(args: &[String], _verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("kubectl"); + let mut cmd = resolved_command("kubectl"); cmd.args(["get", "pods", "-o", "json"]); for arg in args { cmd.arg(arg); @@ -285,7 +285,7 @@ fn kubectl_pods(args: &[String], _verbose: u8) -> Result<()> { fn kubectl_services(args: &[String], _verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("kubectl"); + let mut cmd = resolved_command("kubectl"); cmd.args(["get", "services", "-o", "json"]); for arg in args { cmd.arg(arg); @@ -365,7 +365,7 @@ fn kubectl_logs(args: &[String], _verbose: u8) -> Result<()> { return Ok(()); } - let mut cmd = Command::new("kubectl"); + let mut cmd = resolved_command("kubectl"); cmd.args(["logs", "--tail", "100", pod]); for arg in args.iter().skip(1) { cmd.arg(arg); @@ -529,7 +529,7 @@ pub fn run_docker_passthrough(args: &[OsString], verbose: u8) -> Result<()> { if verbose > 0 { eprintln!("docker passthrough: {:?}", args); } - let status = Command::new("docker") + let status = resolved_command("docker") .args(args) .status() .context("Failed to run docker")?; @@ -551,7 +551,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Raw output for token tracking - let raw_output = Command::new("docker") + let raw_output = resolved_command("docker") .args(["compose", "ps"]) .output() .context("Failed to run docker compose ps")?; @@ -564,7 +564,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> { let raw = String::from_utf8_lossy(&raw_output.stdout).to_string(); // Structured output for parsing (same pattern as docker_ps) - let output = Command::new("docker") + let output = resolved_command("docker") .args([ "compose", "ps", @@ -595,7 +595,7 @@ pub fn run_compose_ps(verbose: u8) -> Result<()> { pub fn run_compose_logs(service: Option<&str>, verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("docker"); + let mut cmd = resolved_command("docker"); cmd.args(["compose", "logs", "--tail", "100"]); if let Some(svc) = service { cmd.arg(svc); @@ -633,7 +633,7 @@ pub fn run_compose_logs(service: Option<&str>, verbose: u8) -> Result<()> { pub fn run_compose_build(service: Option<&str>, verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("docker"); + let mut cmd = resolved_command("docker"); cmd.args(["compose", "build"]); if let Some(svc) = service { cmd.arg(svc); @@ -674,7 +674,7 @@ pub fn run_compose_passthrough(args: &[OsString], verbose: u8) -> Result<()> { if verbose > 0 { eprintln!("docker compose passthrough: {:?}", args); } - let status = Command::new("docker") + let status = resolved_command("docker") .arg("compose") .args(args) .status() @@ -699,7 +699,7 @@ pub fn run_kubectl_passthrough(args: &[OsString], verbose: u8) -> Result<()> { if verbose > 0 { eprintln!("kubectl passthrough: {:?}", args); } - let status = Command::new("kubectl") + let status = resolved_command("kubectl") .args(args) .status() .context("Failed to run kubectl")?; diff --git a/src/curl_cmd.rs b/src/curl_cmd.rs index 60340ea0..90be2236 100644 --- a/src/curl_cmd.rs +++ b/src/curl_cmd.rs @@ -1,12 +1,11 @@ use crate::json_cmd; use crate::tracking; -use crate::utils::truncate; +use crate::utils::{resolved_command, truncate}; use anyhow::{Context, Result}; -use std::process::Command; pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("curl"); + let mut cmd = resolved_command("curl"); cmd.arg("-s"); // Silent mode (no progress bar) for arg in args { diff --git a/src/format_cmd.rs b/src/format_cmd.rs index 10d756ae..c2de5d71 100644 --- a/src/format_cmd.rs +++ b/src/format_cmd.rs @@ -1,10 +1,9 @@ use crate::prettier_cmd; use crate::ruff_cmd; use crate::tracking; -use crate::utils::package_manager_exec; +use crate::utils::{package_manager_exec, resolved_command}; use anyhow::{Context, Result}; use std::path::Path; -use std::process::Command; /// Detect formatter from project files or explicit argument fn detect_formatter(args: &[String]) -> String { @@ -72,9 +71,9 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { // Build command based on formatter let mut cmd = match formatter.as_str() { "prettier" => package_manager_exec("prettier"), - "black" | "ruff" => Command::new(formatter.as_str()), + "black" | "ruff" => resolved_command(formatter.as_str()), "biome" => package_manager_exec("biome"), - _ => Command::new(formatter.as_str()), + _ => resolved_command(formatter.as_str()), }; // Add formatter-specific flags diff --git a/src/golangci_cmd.rs b/src/golangci_cmd.rs index ca55a0ce..1b0a893e 100644 --- a/src/golangci_cmd.rs +++ b/src/golangci_cmd.rs @@ -1,9 +1,8 @@ use crate::tracking; -use crate::utils::truncate; +use crate::utils::{resolved_command, truncate}; use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::HashMap; -use std::process::Command; #[derive(Debug, Deserialize)] struct Position { @@ -34,7 +33,7 @@ struct GolangciOutput { pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("golangci-lint"); + let mut cmd = resolved_command("golangci-lint"); // Force JSON output let has_format = args diff --git a/src/grep_cmd.rs b/src/grep_cmd.rs index 03c5a850..5dee5bed 100644 --- a/src/grep_cmd.rs +++ b/src/grep_cmd.rs @@ -1,8 +1,8 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; use regex::Regex; use std::collections::HashMap; -use std::process::Command; pub fn run( pattern: &str, @@ -23,7 +23,7 @@ pub fn run( // Fix: convert BRE alternation \| → | for rg (which uses PCRE-style regex) let rg_pattern = pattern.replace(r"\|", "|"); - let mut rg_cmd = Command::new("rg"); + let mut rg_cmd = resolved_command("rg"); rg_cmd.args(["-n", "--no-heading", &rg_pattern, path]); if let Some(ft) = file_type { @@ -40,7 +40,11 @@ pub fn run( let output = rg_cmd .output() - .or_else(|_| Command::new("grep").args(["-rn", pattern, path]).output()) + .or_else(|_| { + resolved_command("grep") + .args(["-rn", pattern, path]) + .output() + }) .context("grep/rg failed")?; let stdout = String::from_utf8_lossy(&output.stdout); diff --git a/src/lint_cmd.rs b/src/lint_cmd.rs index 4752524a..267a21e9 100644 --- a/src/lint_cmd.rs +++ b/src/lint_cmd.rs @@ -1,12 +1,10 @@ use crate::mypy_cmd; use crate::ruff_cmd; use crate::tracking; -use crate::utils::{package_manager_exec, truncate}; +use crate::utils::{package_manager_exec, resolved_command, truncate}; use anyhow::{Context, Result}; -use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::process::Command; #[derive(Debug, Deserialize, Serialize)] struct EslintMessage { @@ -90,10 +88,10 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let (linter, explicit) = detect_linter(effective_args); - // Python linters use Command::new() directly (they're on PATH via pip/pipx) + // Python linters 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) { - Command::new(linter) + resolved_command(linter) } else { package_manager_exec(linter) }; diff --git a/src/ls.rs b/src/ls.rs index a6b9df33..e02c215c 100644 --- a/src/ls.rs +++ b/src/ls.rs @@ -1,6 +1,6 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; -use std::process::Command; /// Noise directories commonly excluded from LLM context const NOISE_DIRS: &[&str] = &[ @@ -51,7 +51,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { // Build ls -la + any extra flags the user passed (e.g. -R) // Strip -l, -a, -h (we handle all of these ourselves) - let mut cmd = Command::new("ls"); + let mut cmd = resolved_command("ls"); cmd.arg("-la"); for flag in &flags { if flag.starts_with("--") { diff --git a/src/main.rs b/src/main.rs index 7a1c0159..f85ec44a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -968,7 +968,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<()> { // Start timer before execution to capture actual command runtime let timer = tracking::TimedExecution::start(); - let status = std::process::Command::new(&args[0]) + let status = utils::resolved_command(&args[0]) .args(&args[1..]) .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::inherit()) @@ -1655,7 +1655,7 @@ fn main() -> Result<()> { _ => { // Passthrough other prisma subcommands let timer = tracking::TimedExecution::start(); - let mut cmd = std::process::Command::new("npx"); + let mut cmd = utils::resolved_command("npx"); for arg in &args { cmd.arg(arg); } @@ -1672,7 +1672,7 @@ fn main() -> Result<()> { } } else { let timer = tracking::TimedExecution::start(); - let status = std::process::Command::new("npx") + let status = utils::resolved_command("npx") .arg("prisma") .status() .context("Failed to run npx prisma")?; @@ -1766,10 +1766,6 @@ fn main() -> Result<()> { } Commands::Proxy { args } => { - use std::io::{Read, Write}; - use std::process::{Command, Stdio}; - use std::thread; - if args.is_empty() { anyhow::bail!( "proxy requires a command to execute\nUsage: rtk proxy [args...]" @@ -1788,7 +1784,7 @@ fn main() -> Result<()> { eprintln!("Proxy mode: {} {}", cmd_name, cmd_args.join(" ")); } - let mut child = Command::new(cmd_name.as_ref()) + let mut child = utils::resolved_command(cmd_name.as_ref()) .args(&cmd_args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/src/mypy_cmd.rs b/src/mypy_cmd.rs index 01a32cff..727493be 100644 --- a/src/mypy_cmd.rs +++ b/src/mypy_cmd.rs @@ -1,17 +1,16 @@ use crate::tracking; -use crate::utils::{strip_ansi, truncate}; +use crate::utils::{resolved_command, strip_ansi, tool_exists, truncate}; use anyhow::{Context, Result}; use regex::Regex; use std::collections::HashMap; -use std::process::Command; pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = if which_command("mypy").is_some() { - Command::new("mypy") + let mut cmd = if tool_exists("mypy") { + resolved_command("mypy") } else { - let mut c = Command::new("python3"); + let mut c = resolved_command("python3"); c.arg("-m").arg("mypy"); c }; @@ -47,17 +46,6 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { std::process::exit(output.status.code().unwrap_or(1)); } -fn which_command(cmd: &str) -> Option { - Command::new("which") - .arg(cmd) - .output() - .ok() - .filter(|o| o.status.success()) - .and_then(|o| String::from_utf8(o.stdout).ok()) - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) -} - struct MypyError { file: String, line: usize, diff --git a/src/next_cmd.rs b/src/next_cmd.rs index 1c9087f1..53564d22 100644 --- a/src/next_cmd.rs +++ b/src/next_cmd.rs @@ -1,23 +1,18 @@ use crate::tracking; -use crate::utils::{strip_ansi, truncate}; +use crate::utils::{resolved_command, strip_ansi, tool_exists, truncate}; use anyhow::{Context, Result}; use regex::Regex; -use std::process::Command; pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Try next directly first, fallback to npx if not found - let next_exists = Command::new("which") - .arg("next") - .output() - .map(|o| o.status.success()) - .unwrap_or(false); + let next_exists = tool_exists("next"); let mut cmd = if next_exists { - Command::new("next") + resolved_command("next") } else { - let mut c = Command::new("npx"); + let mut c = resolved_command("npx"); c.arg("next"); c }; diff --git a/src/npm_cmd.rs b/src/npm_cmd.rs index 639cbfde..d0607893 100644 --- a/src/npm_cmd.rs +++ b/src/npm_cmd.rs @@ -1,11 +1,11 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; -use std::process::Command; pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("npm"); + let mut cmd = resolved_command("npm"); cmd.arg("run"); for arg in args { diff --git a/src/pip_cmd.rs b/src/pip_cmd.rs index f595b547..359aef31 100644 --- a/src/pip_cmd.rs +++ b/src/pip_cmd.rs @@ -1,7 +1,7 @@ use crate::tracking; +use crate::utils::{resolved_command, tool_exists}; use anyhow::{Context, Result}; use serde::Deserialize; -use std::process::Command; #[derive(Debug, Deserialize)] struct Package { @@ -15,7 +15,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Auto-detect uv vs pip - let use_uv = which_command("uv").is_some(); + let use_uv = tool_exists("uv"); let base_cmd = if use_uv { "uv" } else { "pip" }; if verbose > 0 && use_uv { @@ -51,7 +51,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { } fn run_list(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> { - let mut cmd = Command::new(base_cmd); + let mut cmd = resolved_command(base_cmd); if base_cmd == "uv" { cmd.arg("pip"); @@ -86,7 +86,7 @@ fn run_list(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, Str } fn run_outdated(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> { - let mut cmd = Command::new(base_cmd); + let mut cmd = resolved_command(base_cmd); if base_cmd == "uv" { cmd.arg("pip"); @@ -121,7 +121,7 @@ fn run_outdated(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, } fn run_passthrough(base_cmd: &str, args: &[String], verbose: u8) -> Result<(String, String)> { - let mut cmd = Command::new(base_cmd); + let mut cmd = resolved_command(base_cmd); if base_cmd == "uv" { cmd.arg("pip"); @@ -153,18 +153,6 @@ fn run_passthrough(base_cmd: &str, args: &[String], verbose: u8) -> Result<(Stri Ok((raw.clone(), raw)) } -/// Check if a command exists in PATH -fn which_command(cmd: &str) -> Option { - Command::new("which") - .arg(cmd) - .output() - .ok() - .filter(|o| o.status.success()) - .and_then(|o| String::from_utf8(o.stdout).ok()) - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) -} - /// Filter pip list JSON output fn filter_pip_list(output: &str) -> String { let packages: Vec = match serde_json::from_str(output) { diff --git a/src/playwright_cmd.rs b/src/playwright_cmd.rs index 3fe915ca..0031ecc3 100644 --- a/src/playwright_cmd.rs +++ b/src/playwright_cmd.rs @@ -1,5 +1,5 @@ use crate::tracking; -use crate::utils::{detect_package_manager, strip_ansi}; +use crate::utils::{detect_package_manager, resolved_command, strip_ansi}; use anyhow::{Context, Result}; use regex::Regex; use serde::Deserialize; @@ -246,17 +246,17 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let pm = detect_package_manager(); let mut cmd = match pm { "pnpm" => { - let mut c = std::process::Command::new("pnpm"); + let mut c = resolved_command("pnpm"); c.arg("exec").arg("--").arg("playwright"); c } "yarn" => { - let mut c = std::process::Command::new("yarn"); + let mut c = resolved_command("yarn"); c.arg("exec").arg("--").arg("playwright"); c } _ => { - let mut c = std::process::Command::new("npx"); + let mut c = resolved_command("npx"); c.arg("--no-install").arg("--").arg("playwright"); c } diff --git a/src/pnpm_cmd.rs b/src/pnpm_cmd.rs index f8399425..50371763 100644 --- a/src/pnpm_cmd.rs +++ b/src/pnpm_cmd.rs @@ -1,9 +1,9 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::HashMap; use std::ffi::OsString; -use std::process::Command; use crate::parser::{ emit_degradation_warning, emit_passthrough_warning, truncate_output, Dependency, @@ -294,7 +294,7 @@ pub fn run(cmd: PnpmCommand, args: &[String], verbose: u8) -> Result<()> { fn run_list(depth: usize, args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("pnpm"); + let mut cmd = resolved_command("pnpm"); cmd.arg("list"); cmd.arg(format!("--depth={}", depth)); cmd.arg("--json"); @@ -350,7 +350,7 @@ fn run_list(depth: usize, args: &[String], verbose: u8) -> Result<()> { fn run_outdated(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("pnpm"); + let mut cmd = resolved_command("pnpm"); cmd.arg("outdated"); cmd.arg("--format"); cmd.arg("json"); @@ -411,7 +411,7 @@ fn run_install(packages: &[String], args: &[String], verbose: u8) -> Result<()> } } - let mut cmd = Command::new("pnpm"); + let mut cmd = resolved_command("pnpm"); cmd.arg("install"); for pkg in packages { @@ -495,7 +495,7 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> { if verbose > 0 { eprintln!("pnpm passthrough: {:?}", args); } - let status = Command::new("pnpm") + let status = resolved_command("pnpm") .args(args) .status() .context("Failed to run pnpm")?; diff --git a/src/prisma_cmd.rs b/src/prisma_cmd.rs index 6fdd5caf..a14684a6 100644 --- a/src/prisma_cmd.rs +++ b/src/prisma_cmd.rs @@ -1,4 +1,5 @@ use crate::tracking; +use crate::utils::{resolved_command, tool_exists}; use anyhow::{Context, Result}; use std::process::Command; @@ -26,16 +27,10 @@ pub fn run(cmd: PrismaCommand, args: &[String], verbose: u8) -> Result<()> { /// Create a Command that will run prisma (tries global first, then npx) fn create_prisma_command() -> Command { - let prisma_exists = Command::new("which") - .arg("prisma") - .output() - .map(|o| o.status.success()) - .unwrap_or(false); - - if prisma_exists { - Command::new("prisma") + if tool_exists("prisma") { + resolved_command("prisma") } else { - let mut c = Command::new("npx"); + let mut c = resolved_command("npx"); c.arg("prisma"); c } diff --git a/src/pytest_cmd.rs b/src/pytest_cmd.rs index 03fe806a..ad694226 100644 --- a/src/pytest_cmd.rs +++ b/src/pytest_cmd.rs @@ -1,7 +1,6 @@ use crate::tracking; -use crate::utils::truncate; +use crate::utils::{resolved_command, tool_exists, truncate}; use anyhow::{Context, Result}; -use std::process::Command; #[derive(Debug, PartialEq)] enum ParseState { @@ -15,11 +14,11 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Try to detect pytest command (could be "pytest", "python -m pytest", etc.) - let mut cmd = if which_command("pytest").is_some() { - Command::new("pytest") + let mut cmd = if tool_exists("pytest") { + resolved_command("pytest") } else { // Fallback to python -m pytest - let mut c = Command::new("python"); + let mut c = resolved_command("python"); c.arg("-m").arg("pytest"); c }; @@ -83,18 +82,6 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -/// Check if a command exists in PATH -fn which_command(cmd: &str) -> Option { - Command::new("which") - .arg(cmd) - .output() - .ok() - .filter(|o| o.status.success()) - .and_then(|o| String::from_utf8(o.stdout).ok()) - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) -} - /// Parse pytest output using state machine fn filter_pytest_output(output: &str) -> String { let mut state = ParseState::Header; diff --git a/src/ruff_cmd.rs b/src/ruff_cmd.rs index 3a58cf51..00df94d3 100644 --- a/src/ruff_cmd.rs +++ b/src/ruff_cmd.rs @@ -1,9 +1,8 @@ use crate::tracking; -use crate::utils::truncate; +use crate::utils::{resolved_command, truncate}; use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::HashMap; -use std::process::Command; #[derive(Debug, Deserialize)] struct RuffLocation { @@ -38,7 +37,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let is_format = args.iter().any(|a| a == "format"); - let mut cmd = Command::new("ruff"); + let mut cmd = resolved_command("ruff"); if is_check { // Force JSON output for check command diff --git a/src/tree.rs b/src/tree.rs index 80449103..39c5ece9 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -7,8 +7,8 @@ //! unless -a flag is present (respecting user intent). use crate::tracking; +use crate::utils::{resolved_command, tool_exists}; use anyhow::{Context, Result}; -use std::process::Command; /// Noise directories commonly excluded from LLM context const NOISE_DIRS: &[&str] = &[ @@ -44,8 +44,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Check if tree is installed - let tree_check = Command::new("which").arg("tree").output(); - if tree_check.is_err() || !tree_check.unwrap().status.success() { + if !tool_exists("tree") { anyhow::bail!( "tree command not found. Install it first:\n\ - macOS: brew install tree\n\ @@ -55,7 +54,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { ); } - let mut cmd = Command::new("tree"); + let mut cmd = resolved_command("tree"); // Determine if user wants all files or default behavior let show_all = args.iter().any(|a| a == "-a" || a == "--all"); diff --git a/src/tsc_cmd.rs b/src/tsc_cmd.rs index 5fabb6d0..14b99b51 100644 --- a/src/tsc_cmd.rs +++ b/src/tsc_cmd.rs @@ -1,5 +1,5 @@ use crate::tracking; -use crate::utils::truncate; +use crate::utils::{resolved_command, tool_exists, truncate}; use anyhow::{Context, Result}; use regex::Regex; use std::collections::HashMap; @@ -9,16 +9,12 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); // Try tsc directly first, fallback to npx if not found - let tsc_exists = Command::new("which") - .arg("tsc") - .output() - .map(|o| o.status.success()) - .unwrap_or(false); + let tsc_exists = tool_exists("tsc"); let mut cmd = if tsc_exists { - Command::new("tsc") + resolved_command("tsc") } else { - let mut c = Command::new("npx"); + let mut c = resolved_command("npx"); c.arg("tsc"); c }; diff --git a/src/utils.rs b/src/utils.rs index afd39334..49c1013a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,9 +7,10 @@ use anyhow::{Context, Result}; use regex::Regex; +use std::path::PathBuf; use std::process::Command; -/// Truncates a string to `max_len` characters, appending "..." if needed. +/// Truncate a string to `max_len` characters, appending "..." if needed. /// /// # Arguments /// * `s` - The string to truncate @@ -33,10 +34,10 @@ pub fn truncate(s: &str, max_len: usize) -> String { } } -/// Strips ANSI escape codes (colors, styles) from a string. +/// Strip ANSI escape codes (colors, styles) from a string. /// /// # Arguments -/// * `text` - Text potentially containing ANSI codes +/// * `text` - Text potentially containing ANSI escape codes /// /// # Examples /// ``` @@ -54,7 +55,7 @@ pub fn strip_ansi(text: &str) -> String { /// Execute a command and return cleaned stdout/stderr. /// /// # Arguments -/// * `cmd` - Command to execute (e.g. "eslint") +/// * `cmd` - Command to execute (e.g., "eslint") /// * `args` - Command arguments /// /// # Returns @@ -68,7 +69,7 @@ pub fn strip_ansi(text: &str) -> String { /// ``` #[allow(dead_code)] pub fn execute_command(cmd: &str, args: &[&str]) -> Result<(String, String, i32)> { - let output = Command::new(cmd) + let output = resolved_command(cmd) .args(args) .output() .context(format!("Failed to execute {}", cmd))?; @@ -83,10 +84,10 @@ pub fn execute_command(cmd: &str, args: &[&str]) -> Result<(String, String, i32) /// Format a token count with K/M suffixes for readability. /// /// # Arguments -/// * `n` - Token count +/// * `n` - Number of tokens /// /// # Returns -/// Formatted string (e.g. "1.2M", "59.2K", "694") +/// Formatted string (e.g., "1.2M", "59.2K", "694") /// /// # Examples /// ``` @@ -229,29 +230,23 @@ pub fn detect_package_manager() -> &'static str { /// Build a Command using the detected package manager's exec mechanism. /// Returns a Command ready to have tool-specific args appended. pub fn package_manager_exec(tool: &str) -> Command { - let tool_exists = Command::new("which") - .arg(tool) - .output() - .map(|o| o.status.success()) - .unwrap_or(false); - - if tool_exists { - Command::new(tool) + if tool_exists(tool) { + resolved_command(tool) } else { let pm = detect_package_manager(); match pm { "pnpm" => { - let mut c = Command::new("pnpm"); + let mut c = resolved_command("pnpm"); c.arg("exec").arg("--").arg(tool); c } "yarn" => { - let mut c = Command::new("yarn"); + let mut c = resolved_command("yarn"); c.arg("exec").arg("--").arg(tool); c } _ => { - let mut c = Command::new("npx"); + let mut c = resolved_command("npx"); c.arg("--no-install").arg("--").arg(tool); c } @@ -259,6 +254,58 @@ pub fn package_manager_exec(tool: &str) -> Command { } } +/// Resolve a binary name to its full path, honoring PATHEXT on Windows. +/// +/// On Windows, Node.js tools are installed as `.CMD`/`.BAT`/`.PS1` shims. +/// Rust's `std::process::Command::new()` does NOT honor PATHEXT, so +/// `Command::new("vitest")` fails even when `vitest.CMD` is on PATH. +/// +/// This function uses the `which` crate to perform proper PATH+PATHEXT resolution. +/// +/// # Arguments +/// * `name` - Binary name (e.g., "vitest", "eslint", "tsc") +/// +/// # Returns +/// Full path to the resolved binary, or error if not found. +pub fn resolve_binary(name: &str) -> Result { + which::which(name).context(format!("Binary '{}' not found on PATH", name)) +} + +/// Create a `Command` with PATHEXT-aware binary resolution. +/// +/// Drop-in replacement for `Command::new(name)` that works on Windows +/// with `.CMD`/`.BAT`/`.PS1` wrappers. +/// +/// Falls back to `Command::new(name)` if resolution fails, so native +/// commands (git, cargo) still work even if `which` can't find them. +/// +/// # Arguments +/// * `name` - Binary name (e.g., "vitest", "eslint") +/// +/// # Returns +/// A `Command` configured with the resolved binary path. +pub fn resolved_command(name: &str) -> Command { + match resolve_binary(name) { + Ok(path) => Command::new(path), + Err(e) => { + // Only log in development/debug builds to avoid stderr pollution + #[cfg(debug_assertions)] + eprintln!( + "rtk: Failed to resolve '{}' via PATH, falling back to direct exec: {}", + name, e + ); + Command::new(name) + } + } +} + +/// Check if a tool exists on PATH (PATHEXT-aware on Windows). +/// +/// Replaces manual `Command::new("which").arg(tool)` checks that fail on Windows. +pub fn tool_exists(name: &str) -> bool { + which::which(name).is_ok() +} + #[cfg(test)] mod tests { use super::*; @@ -429,4 +476,213 @@ mod tests { let result = truncate(cjk, 6); assert!(result.ends_with("...")); } + + // ===== resolve_binary tests (issue #212) ===== + + #[test] + fn test_resolve_binary_finds_known_command() { + // "cargo" must be on PATH in any Rust dev environment + let result = resolve_binary("cargo"); + assert!( + result.is_ok(), + "resolve_binary('cargo') should succeed, got: {:?}", + result.err() + ); + } + + #[test] + fn test_resolve_binary_returns_absolute_path() { + let path = resolve_binary("cargo").expect("cargo should be resolvable"); + assert!( + path.is_absolute(), + "resolve_binary should return absolute path, got: {:?}", + path + ); + } + + #[test] + fn test_resolve_binary_fails_for_unknown() { + let result = resolve_binary("nonexistent_binary_xyz_99999"); + assert!( + result.is_err(), + "resolve_binary should fail for nonexistent binary" + ); + } + + #[test] + fn test_resolve_binary_path_contains_binary_name() { + let path = resolve_binary("cargo").expect("cargo should be resolvable"); + let filename = path + .file_name() + .expect("should have filename") + .to_string_lossy(); + // On Windows this could be "cargo.exe", on Unix just "cargo" + assert!( + filename.starts_with("cargo"), + "resolved path filename should start with 'cargo', got: {}", + filename + ); + } + + // ===== resolved_command tests (issue #212) ===== + + #[test] + fn test_resolved_command_executes_known_command() { + let output = resolved_command("cargo") + .arg("--version") + .output() + .expect("resolved_command('cargo') should execute"); + assert!( + output.status.success(), + "cargo --version should succeed via resolved_command" + ); + } + + // ===== tool_exists tests (issue #212) ===== + + #[test] + fn test_tool_exists_finds_cargo() { + assert!( + tool_exists("cargo"), + "tool_exists('cargo') should return true" + ); + } + + #[test] + fn test_tool_exists_rejects_unknown() { + assert!( + !tool_exists("nonexistent_binary_xyz_99999"), + "tool_exists should return false for nonexistent binary" + ); + } + + #[test] + fn test_tool_exists_finds_git() { + assert!(tool_exists("git"), "tool_exists('git') should return true"); + } + + // ===== Windows-specific PATHEXT resolution tests (issue #212) ===== + + #[cfg(target_os = "windows")] + mod windows_tests { + use super::super::*; + use std::fs; + + /// Create a temporary .cmd wrapper to simulate Node.js tool installation + fn create_temp_cmd_wrapper(dir: &std::path::Path, name: &str) -> std::path::PathBuf { + let cmd_path = dir.join(format!("{}.cmd", name)); + fs::write(&cmd_path, "@echo off\r\necho fake-tool-output\r\n") + .expect("failed to create .cmd wrapper"); + cmd_path + } + + /// Build a PATH string that includes the temp dir + fn path_with_dir(dir: &std::path::Path) -> std::ffi::OsString { + let original = std::env::var_os("PATH").unwrap_or_default(); + let mut new_path = std::ffi::OsString::from(dir.as_os_str()); + new_path.push(";"); + new_path.push(&original); + new_path + } + + #[test] + fn test_resolve_binary_finds_cmd_wrapper() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + create_temp_cmd_wrapper(temp_dir.path(), "fake-tool-test"); + + // Use which::which_in to avoid mutating global PATH (thread-safe) + let search_path = path_with_dir(temp_dir.path()); + let result = which::which_in( + "fake-tool-test", + Some(search_path), + std::env::current_dir().unwrap(), + ); + + assert!( + result.is_ok(), + "which_in should find .cmd wrapper on Windows, got: {:?}", + result.err() + ); + + let path = result.unwrap(); + let ext = path + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + assert!( + ext == "cmd" || ext == "bat", + "resolved path should have .cmd/.bat extension, got: {:?}", + path + ); + } + + #[test] + fn test_resolve_binary_finds_bat_wrapper() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + let bat_path = temp_dir.path().join("fake-bat-tool.bat"); + fs::write(&bat_path, "@echo off\r\necho bat-output\r\n") + .expect("failed to create .bat wrapper"); + + let search_path = path_with_dir(temp_dir.path()); + let result = which::which_in( + "fake-bat-tool", + Some(search_path), + std::env::current_dir().unwrap(), + ); + + assert!( + result.is_ok(), + "which_in should find .bat wrapper on Windows, got: {:?}", + result.err() + ); + } + + #[test] + fn test_resolved_command_executes_cmd_wrapper() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + create_temp_cmd_wrapper(temp_dir.path(), "fake-exec-test"); + + // Resolve the full path, then execute it directly (no PATH mutation) + let search_path = path_with_dir(temp_dir.path()); + let resolved = which::which_in( + "fake-exec-test", + Some(search_path), + std::env::current_dir().unwrap(), + ) + .expect("should resolve fake-exec-test"); + + let output = Command::new(&resolved).output(); + + assert!( + output.is_ok(), + "Command with resolved path should execute .cmd wrapper on Windows" + ); + let output = output.unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("fake-tool-output"), + "should get output from .cmd wrapper, got: {}", + stdout + ); + } + + #[test] + fn test_tool_exists_finds_cmd_wrapper() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + create_temp_cmd_wrapper(temp_dir.path(), "fake-exists-test"); + + let search_path = path_with_dir(temp_dir.path()); + let result = which::which_in( + "fake-exists-test", + Some(search_path), + std::env::current_dir().unwrap(), + ); + + assert!( + result.is_ok(), + "which_in should find .cmd wrapper on Windows" + ); + } + } } diff --git a/src/wc_cmd.rs b/src/wc_cmd.rs index d827eb89..6ac16192 100644 --- a/src/wc_cmd.rs +++ b/src/wc_cmd.rs @@ -7,13 +7,13 @@ /// - `wc -c file.py` → `978` /// - `wc -l *.py` → table with common path prefix stripped use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; -use std::process::Command; pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("wc"); + let mut cmd = resolved_command("wc"); for arg in args { cmd.arg(arg); } diff --git a/src/wget_cmd.rs b/src/wget_cmd.rs index b2720052..548f94a8 100644 --- a/src/wget_cmd.rs +++ b/src/wget_cmd.rs @@ -1,6 +1,6 @@ use crate::tracking; +use crate::utils::resolved_command; use anyhow::{Context, Result}; -use std::process::Command; /// Compact wget - strips progress bars, shows only result pub fn run(url: &str, args: &[String], verbose: u8) -> Result<()> { @@ -19,7 +19,7 @@ pub fn run(url: &str, args: &[String], verbose: u8) -> Result<()> { } cmd_args.push(url); - let output = Command::new("wget") + let output = resolved_command("wget") .args(&cmd_args) .output() .context("Failed to run wget")?; @@ -64,7 +64,7 @@ pub fn run_stdout(url: &str, args: &[String], verbose: u8) -> Result<()> { } cmd_args.push(url); - let output = Command::new("wget") + let output = resolved_command("wget") .args(&cmd_args) .output() .context("Failed to run wget")?; From 7594475e5dd1570a20f65c6e4f793e506cc1e4d0 Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 11:12:04 +0100 Subject: [PATCH 2/8] =?UTF-8?q?fix(integrity):=20=F0=9F=90=9B=20improve=20?= =?UTF-8?q?error=20handling=20and=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor error messages for clarity in `read_stored_hash`. - Simplify integrity check output in `runtime_check`. --- src/init.rs | 11 +++++++---- src/integrity.rs | 20 ++++---------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/init.rs b/src/init.rs index 3ff2622b..9bc87e11 100644 --- a/src/init.rs +++ b/src/init.rs @@ -228,8 +228,12 @@ fn ensure_hook_installed(hook_path: &Path, verbose: u8) -> Result { // Store SHA-256 hash for runtime integrity verification. // Always store (idempotent) to ensure baseline exists even for // hooks installed before integrity checks were added. - integrity::store_hash(hook_path) - .with_context(|| format!("Failed to store integrity hash for {}", hook_path.display()))?; + integrity::store_hash(hook_path).with_context(|| { + format!( + "Failed to store integrity hash for {}", + hook_path.display() + ) + })?; if verbose > 0 && changed { eprintln!("Stored integrity hash for hook"); } @@ -1081,8 +1085,7 @@ pub fn show_config() -> Result<()> { Ok(integrity::IntegrityStatus::NoBaseline) => { println!("⚠️ Integrity: no baseline hash (run: rtk init -g to establish)"); } - Ok(integrity::IntegrityStatus::NotInstalled) - | Ok(integrity::IntegrityStatus::OrphanedHash) => { + Ok(integrity::IntegrityStatus::NotInstalled) | Ok(integrity::IntegrityStatus::OrphanedHash) => { // Don't show integrity line if hook isn't installed } Err(_) => { diff --git a/src/integrity.rs b/src/integrity.rs index 41bcf4e8..945d4ae3 100644 --- a/src/integrity.rs +++ b/src/integrity.rs @@ -164,10 +164,7 @@ fn read_stored_hash(path: &Path) -> Result { // sha256sum format uses two-space separator: " " let parts: Vec<&str> = line.splitn(2, " ").collect(); if parts.len() != 2 { - anyhow::bail!( - "Invalid hash format in {} (expected 'hash filename')", - path.display() - ); + anyhow::bail!("Invalid hash format in {} (expected 'hash filename')", path.display()); } let hash = parts[0]; @@ -253,14 +250,8 @@ pub fn runtime_check() -> Result<()> { } IntegrityStatus::Tampered { expected, actual } => { eprintln!("rtk: hook integrity check FAILED"); - eprintln!( - " Expected hash: {}...", - expected.get(..16).unwrap_or(&expected) - ); - eprintln!( - " Actual hash: {}...", - actual.get(..16).unwrap_or(&actual) - ); + eprintln!(" Expected hash: {}...", expected.get(..16).unwrap_or(&expected)); + eprintln!(" Actual hash: {}...", actual.get(..16).unwrap_or(&actual)); eprintln!(); eprintln!(" The hook at ~/.claude/hooks/rtk-rewrite.sh has been modified."); eprintln!(" This may indicate tampering. RTK will not execute."); @@ -492,10 +483,7 @@ mod tests { .unwrap(); let result = verify_hook_at(&hook); - assert!( - result.is_err(), - "Should reject hash-only format (no filename)" - ); + assert!(result.is_err(), "Should reject hash-only format (no filename)"); } #[test] From c02df3838fab8d69f1fb8ea1730464592718bd58 Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 11:23:07 +0100 Subject: [PATCH 3/8] =?UTF-8?q?chore(tsc=5Fcmd):=20=F0=9F=94=A7=20remove?= =?UTF-8?q?=20unused=20import=20of=20`std::process::Command`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tsc_cmd.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tsc_cmd.rs b/src/tsc_cmd.rs index 14b99b51..ad4658e1 100644 --- a/src/tsc_cmd.rs +++ b/src/tsc_cmd.rs @@ -3,7 +3,6 @@ use crate::utils::{resolved_command, tool_exists, truncate}; use anyhow::{Context, Result}; use regex::Regex; use std::collections::HashMap; -use std::process::Command; pub fn run(args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); From 390610c5e28021479999e3cdf64876cf7c6ce0b6 Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 11:26:04 +0100 Subject: [PATCH 4/8] =?UTF-8?q?fix(utils):=20=F0=9F=90=9B=20improve=20comm?= =?UTF-8?q?and=20resolution=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - On Windows, always warn when a .CMD/.BAT wrapper isn't found. - On Unix, log only in debug builds to reduce noise. --- src/utils.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 49c1013a..8b53806f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -288,12 +288,22 @@ pub fn resolved_command(name: &str) -> Command { match resolve_binary(name) { Ok(path) => Command::new(path), Err(e) => { - // Only log in development/debug builds to avoid stderr pollution - #[cfg(debug_assertions)] + // On Windows, resolution failure likely means a .CMD/.BAT wrapper + // wasn't found — always warn so users have a signal. + // On Unix, this is less common; only log in debug builds. + #[cfg(target_os = "windows")] eprintln!( "rtk: Failed to resolve '{}' via PATH, falling back to direct exec: {}", name, e ); + #[cfg(not(target_os = "windows"))] + { + #[cfg(debug_assertions)] + eprintln!( + "rtk: Failed to resolve '{}' via PATH, falling back to direct exec: {}", + name, e + ); + } Command::new(name) } } From 99c21df25e3513bec1f48c6f91ca8a21d7325989 Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 11:26:43 +0100 Subject: [PATCH 5/8] =?UTF-8?q?chore(gitignore):=20=F0=9F=94=A7=20add=20`.?= =?UTF-8?q?omc`=20to=20ignored=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e68eca84..38292a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ benchmark-report.md *.sqlite *.sqlite3 rtk_tracking.db -claudedocs \ No newline at end of file +claudedocs +.omc \ No newline at end of file From 58b2dfeebf8ee16c43770ed434e7e04318aa148f Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 11:40:18 +0100 Subject: [PATCH 6/8] =?UTF-8?q?chore(dependencies):=20=F0=9F=94=A7=20updat?= =?UTF-8?q?e=20dependencies=20in=20`Cargo.lock`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump versions for several packages to latest releases. - Ensure compatibility and security improvements with updated dependencies. --- Cargo.lock | 349 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 277 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2a2a829..2ef7050d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "autocfg" @@ -108,9 +108,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -133,15 +133,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -155,9 +155,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -312,6 +312,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_home" version = "0.1.0" @@ -354,9 +365,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" @@ -368,6 +379,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -400,14 +417,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -432,6 +450,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -466,9 +493,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -569,6 +596,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -614,6 +647,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -630,9 +665,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -644,19 +679,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags", "libc", ] @@ -673,9 +713,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -691,9 +731,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -753,6 +793,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -764,18 +814,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_users" @@ -790,9 +840,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -802,9 +852,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -813,9 +863,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -872,9 +922,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -933,6 +983,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1035,9 +1091,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1057,12 +1113,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1147,9 +1203,15 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -1234,11 +1296,20 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1249,9 +1320,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1259,9 +1330,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1272,18 +1343,70 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" dependencies = [ "env_home", "rustix", @@ -1517,9 +1640,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -1535,6 +1658,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -1567,18 +1772,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -1647,6 +1852,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 8bc541b30db9bceda83f6449d36e55870d00c937 Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 12:03:36 +0100 Subject: [PATCH 7/8] fix(main): restore local imports removed during resolved_command migration --- src/init.rs | 11 ++++------- src/integrity.rs | 20 ++++++++++++++++---- src/main.rs | 4 ++++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/init.rs b/src/init.rs index 9bc87e11..3ff2622b 100644 --- a/src/init.rs +++ b/src/init.rs @@ -228,12 +228,8 @@ fn ensure_hook_installed(hook_path: &Path, verbose: u8) -> Result { // Store SHA-256 hash for runtime integrity verification. // Always store (idempotent) to ensure baseline exists even for // hooks installed before integrity checks were added. - integrity::store_hash(hook_path).with_context(|| { - format!( - "Failed to store integrity hash for {}", - hook_path.display() - ) - })?; + integrity::store_hash(hook_path) + .with_context(|| format!("Failed to store integrity hash for {}", hook_path.display()))?; if verbose > 0 && changed { eprintln!("Stored integrity hash for hook"); } @@ -1085,7 +1081,8 @@ pub fn show_config() -> Result<()> { Ok(integrity::IntegrityStatus::NoBaseline) => { println!("⚠️ Integrity: no baseline hash (run: rtk init -g to establish)"); } - Ok(integrity::IntegrityStatus::NotInstalled) | Ok(integrity::IntegrityStatus::OrphanedHash) => { + Ok(integrity::IntegrityStatus::NotInstalled) + | Ok(integrity::IntegrityStatus::OrphanedHash) => { // Don't show integrity line if hook isn't installed } Err(_) => { diff --git a/src/integrity.rs b/src/integrity.rs index 945d4ae3..41bcf4e8 100644 --- a/src/integrity.rs +++ b/src/integrity.rs @@ -164,7 +164,10 @@ fn read_stored_hash(path: &Path) -> Result { // sha256sum format uses two-space separator: " " let parts: Vec<&str> = line.splitn(2, " ").collect(); if parts.len() != 2 { - anyhow::bail!("Invalid hash format in {} (expected 'hash filename')", path.display()); + anyhow::bail!( + "Invalid hash format in {} (expected 'hash filename')", + path.display() + ); } let hash = parts[0]; @@ -250,8 +253,14 @@ pub fn runtime_check() -> Result<()> { } IntegrityStatus::Tampered { expected, actual } => { eprintln!("rtk: hook integrity check FAILED"); - eprintln!(" Expected hash: {}...", expected.get(..16).unwrap_or(&expected)); - eprintln!(" Actual hash: {}...", actual.get(..16).unwrap_or(&actual)); + eprintln!( + " Expected hash: {}...", + expected.get(..16).unwrap_or(&expected) + ); + eprintln!( + " Actual hash: {}...", + actual.get(..16).unwrap_or(&actual) + ); eprintln!(); eprintln!(" The hook at ~/.claude/hooks/rtk-rewrite.sh has been modified."); eprintln!(" This may indicate tampering. RTK will not execute."); @@ -483,7 +492,10 @@ mod tests { .unwrap(); let result = verify_hook_at(&hook); - assert!(result.is_err(), "Should reject hash-only format (no filename)"); + assert!( + result.is_err(), + "Should reject hash-only format (no filename)" + ); } #[test] diff --git a/src/main.rs b/src/main.rs index f85ec44a..4d957492 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1766,6 +1766,10 @@ fn main() -> Result<()> { } Commands::Proxy { args } => { + use std::io::{Read, Write}; + use std::process::Stdio; + use std::thread; + if args.is_empty() { anyhow::bail!( "proxy requires a command to execute\nUsage: rtk proxy [args...]" From 15d4159008ffbf90cef2b1a19b0c177e2c14674e Mon Sep 17 00:00:00 2001 From: Artiom Tofan Date: Fri, 6 Mar 2026 12:16:15 +0100 Subject: [PATCH 8/8] =?UTF-8?q?test(tests):=20=F0=9F=A7=AA=20add=20fallbac?= =?UTF-8?q?k=20test=20for=20unknown=20binary=20in=20`resolved=5Fcommand`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure that `resolved_command` does not panic when falling back to `Command::new(name)` for unknown binaries. This test verifies that the command can be created without errors, even if execution fails. --- src/utils.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index 8b53806f..0bb5db07 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -677,6 +677,22 @@ mod tests { ); } + #[test] + fn test_resolved_command_fallback_on_unknown_binary() { + // When resolve_binary fails, resolved_command should fall back to + // Command::new(name) instead of panicking. On Windows this also + // prints a warning to stderr. + let mut cmd = resolved_command("nonexistent_binary_xyz_99999"); + // The Command should be created (not panic). Attempting to run it + // will fail, but that's expected — we just verify the fallback path + // produces a usable Command. + let result = cmd.output(); + assert!( + result.is_err() || !result.unwrap().status.success(), + "nonexistent binary should fail to execute, but resolved_command must not panic" + ); + } + #[test] fn test_tool_exists_finds_cmd_wrapper() { let temp_dir = tempfile::tempdir().expect("failed to create temp dir");