Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/analytics/session_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ pub fn run(_verbose: u8) -> Result<()> {
if sessions.is_empty() {
println!("No Claude Code sessions found in the last 30 days.");
println!("Make sure Claude Code has been used at least once.");

// On Windows, the session directory may not exist or be in a different location
if cfg!(target_os = "windows") {
println!("\n[rtk] Note: On Windows, Claude Code sessions are stored in:");
println!(" %USERPROFILE%\\.claude\\projects\\");
println!("If this directory doesn't exist, Claude Code may not have been run yet,");
println!("or the sessions may be stored in a different location.");
}

return Ok(());
}

Expand Down
29 changes: 26 additions & 3 deletions src/cmds/git/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,15 @@ fn run_log(
arg.starts_with("--oneline") || arg.starts_with("--pretty") || arg.starts_with("--format")
});

// Check if user provided limit flag (-N, -n N, --max-count=N, --max-count N)
// Check if user provided limit flag (-N, -n N, -nN, --max-count=N, --max-count N)
let has_limit_flag = args.iter().any(|arg| {
(arg.starts_with('-') && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
// -N form (e.g., -20)
(arg.starts_with('-') && arg.len() > 1 && arg.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
// -nN form (e.g., -n20)
|| (arg.starts_with("-n") && arg.len() > 2 && arg.chars().nth(2).is_some_and(|c| c.is_ascii_digit()))
// -n as standalone token
|| arg == "-n"
// --max-count forms
|| arg.starts_with("--max-count")
});

Expand Down Expand Up @@ -507,7 +512,7 @@ fn run_log(

/// Filter git log output: truncate long messages, cap lines
/// Parse the user-specified limit from git log args.
/// Handles: -20, -n 20, --max-count=20, --max-count 20
/// Handles: -20, -n 20, -n, -n20, --max-count=20, --max-count 20
fn parse_user_limit(args: &[String]) -> Option<usize> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
Expand All @@ -520,6 +525,12 @@ fn parse_user_limit(args: &[String]) -> Option<usize> {
return Some(n);
}
}
// -n20 (combined -nN form)
if arg.starts_with("-n") && arg.len() > 2 && arg.chars().nth(2).is_some_and(|c| c.is_ascii_digit()) {
if let Ok(n) = arg[2..].parse::<usize>() {
return Some(n);
}
}
// -n 20 (two-token form)
if arg == "-n" {
if let Some(next) = iter.next() {
Expand Down Expand Up @@ -2372,6 +2383,18 @@ A added.rs
assert_eq!(parse_user_limit(&args), Some(25));
}

#[test]
fn test_parse_user_limit_n_combined() {
let args: Vec<String> = vec!["-n20".into()];
assert_eq!(parse_user_limit(&args), Some(20));
}

#[test]
fn test_parse_user_limit_n_combined_single_digit() {
let args: Vec<String> = vec!["-n5".into()];
assert_eq!(parse_user_limit(&args), Some(5));
}

#[test]
fn test_parse_user_limit_none() {
let args: Vec<String> = vec!["--oneline".into()];
Expand Down
17 changes: 17 additions & 0 deletions src/cmds/js/npm_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use anyhow::Result;

/// Known npm subcommands that should NOT get "run" injected.
/// Shared between production code and tests to avoid drift.
/// Updated to include all 15 missing subcommands from npm 10.8.2 (issue #2663).
const NPM_SUBCOMMANDS: &[&str] = &[
"install",
"i",
Expand Down Expand Up @@ -71,6 +72,22 @@ const NPM_SUBCOMMANDS: &[&str] = &[
"start",
"stop",
"restart",
// Added in fix for #2663 - 15 missing official npm subcommands
"completion",
"edit",
"explore",
"find-dupes",
"help-search",
"hook",
"install-ci-test",
"install-test",
"ll",
"org",
"query",
"run-script",
"sbom",
"shrinkwrap",
"unstar",
];

pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<i32> {
Expand Down
22 changes: 21 additions & 1 deletion src/cmds/python/ruff_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,30 @@ struct RuffDiagnostic {
fix: Option<RuffFix>,
}

/// Known ruff subcommands that should NOT get "check --output-format=json" injected.
/// Based on `ruff --help` output. Only "check" and "format" need special handling;
/// all other subcommands pass through unmodified.
const RUFF_SUBCOMMANDS: &[&str] = &[
"check",
"format",
"version",
"rule",
"config",
"linter",
"clean",
"server",
"analyze",
"generate-shell-completion",
"help",
];

pub fn run(args: &[String], verbose: u8) -> Result<i32> {
// Determine if this is a "check" invocation (needs JSON output for filtering)
// Only "check" subcommand or path-like arguments trigger the check filter.
let is_check = args.is_empty()
|| args[0] == "check"
|| (!args[0].starts_with('-') && args[0] != "format" && args[0] != "version");
|| (!args[0].starts_with('-')
&& !RUFF_SUBCOMMANDS.contains(&args[0].as_str()));

let is_format = args.iter().any(|a| a == "format");

Expand Down
12 changes: 11 additions & 1 deletion src/cmds/system/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use super::constants::NOISE_DIRS;
use crate::core::runner::{self, RunOptions};
use crate::core::truncate::{reduced, CAP_WARNINGS};
use crate::core::utils::resolved_command;
use crate::core::utils::{resolved_command, tool_exists};
use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
Expand Down Expand Up @@ -48,6 +48,16 @@ pub fn run(args: &[String], verbose: u8) -> Result<i32> {
.map(|s| s.as_str())
.collect();

// On Windows, `ls` is a PowerShell alias, not an executable on PATH.
// Suggest `rtk tree` as a native alternative.
if cfg!(target_os = "windows") && !tool_exists("ls") {
eprintln!(
"[rtk] Note: 'ls' is a PowerShell alias on Windows, not a native executable.\n\
For directory listings on Windows, use 'rtk tree .' instead."
);
// Fall through to let resolved_command fail with its own message
}

let mut cmd = resolved_command("ls");
cmd.env("LC_ALL", "C");
cmd.arg("-la");
Expand Down
14 changes: 13 additions & 1 deletion src/cmds/system/wc_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@
/// - `wc -c file.py` → `978`
/// - `wc -l *.py` → table with common path prefix stripped
use crate::core::runner::{self, RunOptions};
use crate::core::utils::resolved_command;
use crate::core::utils::{resolved_command, tool_exists};
use anyhow::Result;

pub fn run(args: &[String], verbose: u8) -> Result<i32> {
// On Windows, `wc` is not a standard executable. Provide a helpful message.
if cfg!(target_os = "windows") && !tool_exists("wc") {
eprintln!(
"[rtk] Note: 'wc' is not available as a standard Windows executable.\n\
For line/word/char counts on Windows, consider:\n\
- PowerShell: Get-Content file.txt | Measure-Object -Line -Word -Character\n\
- Git Bash/WSL: wc (available in Unix-like environments)\n\
- Native alternative: find /c /v \"\" file.txt (line count only)"
);
// Fall through to let resolved_command fail with its own message
}

let mut cmd = resolved_command("wc");
for arg in args {
cmd.arg(arg);
Expand Down
7 changes: 7 additions & 0 deletions src/discover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ pub fn run(
}
}

// On Windows, if no sessions found, provide helpful info about session location
if cfg!(target_os = "windows") && sessions.is_empty() {
eprintln!(
"\n[rtk] Note: On Windows, Claude Code sessions are stored in:\n %USERPROFILE%\\.claude\\projects\\\nIf this directory doesn't exist, Claude Code may not have been run yet,\nor sessions may be in a different location."
);
}

let mut total_commands: usize = 0;
let mut already_rtk: usize = 0;
let mut parse_errors: usize = 0;
Expand Down
25 changes: 25 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,31 @@ fn run_cli() -> Result<i32> {
)
};

// On Windows, warn about PowerShell aliases and built-ins that aren't real executables.
// These commands will fail because RTK resolves executables on PATH, not shell aliases/built-ins.
if cfg!(target_os = "windows") {
let powershell_aliases = [
"ls", "dir", "echo", "pwd", "cp", "mv", "rm", "cat", "man", "which",
];
let powershell_builtins =
["cd", "set", "exit", "cls", "history", "alias", "function"];

if powershell_aliases.contains(&cmd_name.as_str()) {
eprintln!(
"[rtk] Note: '{}' is a PowerShell alias, not a native executable on PATH.\n\
rtk proxy only works with real executables. Try running the command directly in PowerShell,\n\
or use the native Windows equivalent.",
cmd_name
);
} else if powershell_builtins.contains(&cmd_name.as_str()) {
eprintln!(
"[rtk] Note: '{}' is a PowerShell built-in command, not an executable on PATH.\n\
rtk proxy cannot execute shell built-ins. Run it directly in PowerShell instead.",
cmd_name
);
}
}

if cli.verbose > 0 {
eprintln!("Proxy mode: {} {}", cmd_name, cmd_args.join(" "));
}
Expand Down