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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/security-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Security Check

on:
pull_request:
branches: [ master, develop ]
branches: [ master ]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.27.2"
".": "0.28.0"
}
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1488,4 +1488,4 @@ When implementing a new command, consider:

**Last Updated**: 2026-02-22
**Architecture Version**: 2.2
**rtk Version**: 0.27.2
**rtk Version**: 0.28.0
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.28.0](https://github.com/rtk-ai/rtk/compare/v0.27.2...v0.28.0) (2026-03-10)


### Features

* **gt:** add Graphite CLI support ([#290](https://github.com/rtk-ai/rtk/issues/290)) ([7fbc4ef](https://github.com/rtk-ai/rtk/commit/7fbc4ef4b553d5e61feeb6e73d8f6a96b6df3dd9))
* TOML Part 1 — filter DSL engine + 14 built-in filters ([#349](https://github.com/rtk-ai/rtk/issues/349)) ([adda253](https://github.com/rtk-ai/rtk/commit/adda2537be1fe69625ac280f15e8c8067d08c711))
* TOML Part 2 — user-global config, shadow warning, rtk init templates, 4 new built-in filters ([#351](https://github.com/rtk-ai/rtk/issues/351)) ([926e6a0](https://github.com/rtk-ai/rtk/commit/926e6a0dd4512c4cbb0f5ac133e60cb6134a3174))
* TOML Part 3 — 15 additional built-in filters (ping, rsync, dotnet, swift, shellcheck, hadolint, poetry, composer, brew, df, ps, systemctl, yamllint, markdownlint, uv) ([#386](https://github.com/rtk-ai/rtk/issues/386)) ([b71a8d2](https://github.com/rtk-ai/rtk/commit/b71a8d24e2dbd3ff9bb423c849638bfa23830c0b))

## [0.27.2](https://github.com/rtk-ai/rtk/compare/v0.27.1...v0.27.2) (2026-03-06)


Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This is a fork with critical fixes for git argument parsing and modern JavaScrip

**Verify correct installation:**
```bash
rtk --version # Should show "rtk 0.27.2" (or newer)
rtk --version # Should show "rtk 0.28.0" (or newer)
rtk gain # Should show token savings stats (NOT "command not found")
```

Expand Down
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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rtk"
version = "0.27.2"
version = "0.28.0"
edition = "2021"
authors = ["Patrick Szymkowiak"]
description = "Rust Token Killer - High-performance CLI proxy to minimize LLM token consumption"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Download from [releases](https://github.com/rtk-ai/rtk/releases):
### Verify Installation

```bash
rtk --version # Should show "rtk 0.27.2"
rtk --version # Should show "rtk 0.28.0"
rtk gain # Should show token savings stats
```

Expand Down
1 change: 1 addition & 0 deletions hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# To add or change rewrite rules, edit the Rust registry — not this file.

if ! command -v jq &>/dev/null; then
echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2
exit 0
fi

Expand Down
50 changes: 45 additions & 5 deletions src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,14 @@ fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Result<()
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

let formatted = if !stderr.is_empty() && stderr.contains("not a git repository") {
"Not a git repository".to_string()
} else {
format_status_output(&stdout)
};
if !stderr.is_empty() && stderr.contains("not a git repository") {
let message = "Not a git repository".to_string();
eprintln!("{}", message);
timer.track("git status", "rtk git status", &raw_output, &message);
std::process::exit(output.status.code().unwrap_or(128));
}

let formatted = format_status_output(&stdout);

println!("{}", formatted);

Expand Down Expand Up @@ -1810,4 +1813,41 @@ no changes added to commit (use "git add" and/or "git commit -a")
.collect();
assert_eq!(cmd_args, vec!["commit", "--amend", "-m", "new msg"]);
}

#[test]
fn test_git_status_not_a_repo_exits_nonzero() {
// Run rtk git status in a directory that is not a git repo
let tmp = std::env::temp_dir().join("rtk_test_not_a_repo");
let _ = std::fs::create_dir_all(&tmp);

// Build the path to the test binary
let bin_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("debug")
.join("rtk");
let output = std::process::Command::new(&bin_path)
.args(["git", "status"])
.current_dir(&tmp)
.output()
.expect("Failed to run rtk");

// Should exit with non-zero (128 from git)
assert!(
!output.status.success(),
"Expected non-zero exit code for git status outside a repo, got {:?}",
output.status.code()
);

// Message should be on stderr, not stdout
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stderr.contains("Not a git repository"),
"Expected 'Not a git repository' on stderr, got stderr={:?}, stdout={:?}",
stderr,
stdout
);

let _ = std::fs::remove_dir_all(&tmp);
}
}
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ enum PnpmCommands {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Build (delegates to next build filter)
/// Build (generic passthrough, no framework-specific filter)
Build {
/// Additional build arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
Expand Down Expand Up @@ -1335,7 +1335,10 @@ fn main() -> Result<()> {
)?;
}
PnpmCommands::Build { args } => {
next_cmd::run(&args, cli.verbose)?;
let mut build_args: Vec<String> = vec!["build".into()];
build_args.extend(args);
let os_args: Vec<OsString> = build_args.into_iter().map(OsString::from).collect();
pnpm_cmd::run_passthrough(&os_args, cli.verbose)?;
}
PnpmCommands::Typecheck { args } => {
tsc_cmd::run(&args, cli.verbose)?;
Expand Down
48 changes: 44 additions & 4 deletions src/npm_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> {
let mut cmd = Command::new("npm");
cmd.arg("run");

for arg in args {
// Strip leading "run" to avoid doubling (rtk npm run build → npm run build, not npm run run build)
let effective_args = if args.first().map(|s| s.as_str()) == Some("run") {
&args[1..]
} else {
args
};

for arg in effective_args {
cmd.arg(arg);
}

Expand All @@ -17,7 +24,7 @@ pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> {
}

if verbose > 0 {
eprintln!("Running: npm run {}", args.join(" "));
eprintln!("Running: npm run {}", effective_args.join(" "));
}

let output = cmd.output().context("Failed to run npm run")?;
Expand All @@ -29,8 +36,8 @@ pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> {
println!("{}", filtered);

timer.track(
&format!("npm run {}", args.join(" ")),
&format!("rtk npm run {}", args.join(" ")),
&format!("npm run {}", effective_args.join(" ")),
&format!("rtk npm run {}", effective_args.join(" ")),
&raw,
&filtered,
);
Expand Down Expand Up @@ -100,6 +107,39 @@ npm notice
assert!(result.contains("Build completed"));
}

#[test]
fn test_strip_leading_run_from_args() {
// When user runs `rtk npm run build`, args = ["run", "build"]
// The "run" should be stripped since cmd.arg("run") already adds it
let args: Vec<String> = vec!["run".into(), "build".into()];
let effective_args = if args.first().map(|s| s.as_str()) == Some("run") {
&args[1..]
} else {
&args[..]
};
assert_eq!(effective_args, &["build"]);

// When user runs `rtk npm build`, args = ["build"]
// No stripping needed
let args2: Vec<String> = vec!["build".into()];
let effective_args2 = if args2.first().map(|s| s.as_str()) == Some("run") {
&args2[1..]
} else {
&args2[..]
};
assert_eq!(effective_args2, &["build"]);

// When user runs `rtk npm run`, args = ["run"]
// Strip "run" → empty args (npm run with no script)
let args3: Vec<String> = vec!["run".into()];
let effective_args3 = if args3.first().map(|s| s.as_str()) == Some("run") {
&args3[1..]
} else {
&args3[..]
};
assert!(effective_args3.is_empty());
}

#[test]
fn test_filter_npm_output_empty() {
let output = "\n\n\n";
Expand Down
Loading
Loading