mod analytics;
mod cmds;
mod core;
mod discover;
mod hooks;
mod learn;
mod parser;
use cmds::cloud::{aws_cmd, container, curl_cmd, psql_cmd, wget_cmd};
use cmds::dotnet::{binlog, dotnet_cmd, dotnet_format_report, dotnet_trx};
use cmds::git::{diff_cmd, gh_cmd, git, glab_cmd, gt_cmd};
use cmds::go::{go_cmd, golangci_cmd};
use cmds::js::{
lint_cmd, next_cmd, npm_cmd, playwright_cmd, pnpm_cmd, prettier_cmd, prisma_cmd, tsc_cmd,
vitest_cmd,
};
use cmds::jvm::gradlew_cmd;
use cmds::python::{mypy_cmd, pip_cmd, pytest_cmd, ruff_cmd};
use cmds::ruby::{rake_cmd, rspec_cmd, rubocop_cmd};
use cmds::rust::{cargo_cmd, runner};
use cmds::system::{
deps, env_cmd, find_cmd, format_cmd, grep_cmd, json_cmd, local_llm, log_cmd, ls, pipe_cmd,
read, summary, tree, wc_cmd,
};
use anyhow::{Context, Result};
use clap::error::ErrorKind;
use clap::{Parser, Subcommand, ValueEnum};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
pub enum AgentTarget {
Claude,
Cursor,
Windsurf,
Cline,
Kilocode,
Antigravity,
Hermes,
}
#[derive(Parser)]
#[command(
name = "rtk",
version,
about = "Rust Token Killer - Minimize LLM token consumption",
long_about = "A high-performance CLI proxy designed to filter and summarize system outputs before they reach your LLM context."
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(long, global = true)]
ultra_compact: bool,
#[arg(long = "skip-env", global = true)]
skip_env: bool,
}
#[derive(Debug, Subcommand)]
enum Commands {
Ls {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Tree {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Read {
#[arg(required = true, num_args = 1..)]
files: Vec<PathBuf>,
#[arg(short, long, default_value = "none")]
level: core::filter::FilterLevel,
#[arg(short, long, conflicts_with = "tail_lines")]
max_lines: Option<usize>,
#[arg(long, conflicts_with = "max_lines")]
tail_lines: Option<usize>,
#[arg(short = 'n', long)]
line_numbers: bool,
},
Smart {
file: PathBuf,
#[arg(short, long, default_value = "heuristic")]
model: String,
#[arg(long)]
force_download: bool,
},
Git {
#[arg(short = 'C', action = clap::ArgAction::Append)]
directory: Vec<String>,
#[arg(short = 'c', action = clap::ArgAction::Append)]
config_override: Vec<String>,
#[arg(long = "git-dir")]
git_dir: Option<String>,
#[arg(long = "work-tree")]
work_tree: Option<String>,
#[arg(long = "no-pager")]
no_pager: bool,
#[arg(long = "no-optional-locks")]
no_optional_locks: bool,
#[arg(long)]
bare: bool,
#[arg(long = "literal-pathspecs")]
literal_pathspecs: bool,
#[command(subcommand)]
command: GitCommands,
},
Gh {
subcommand: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Glab {
#[arg(short = 'R', long = "repo")]
repo: Option<String>,
#[arg(short = 'g', long = "group")]
group: Option<String>,
subcommand: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Aws {
subcommand: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(disable_help_flag = true)]
Psql {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pnpm {
#[arg(long, short = 'F')]
filter: Vec<String>,
#[command(subcommand)]
command: PnpmCommands,
},
Err {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Test {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Json {
file: PathBuf,
#[arg(short, long, default_value = "5")]
depth: usize,
#[arg(long)]
keys_only: bool,
},
Deps {
#[arg(default_value = ".")]
path: PathBuf,
},
Env {
#[arg(short, long)]
filter: Option<String>,
#[arg(long)]
show_all: bool,
},
Find {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Diff {
file1: PathBuf,
file2: Option<PathBuf>,
},
Log {
file: Option<PathBuf>,
},
Dotnet {
#[command(subcommand)]
command: DotnetCommands,
},
Docker {
#[command(subcommand)]
command: DockerCommands,
},
Kubectl {
#[command(subcommand)]
command: KubectlCommands,
},
Summary {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Grep {
pattern: String,
#[arg(default_value = ".")]
path: String,
#[arg(short = 'l', long, default_value = "80")]
max_len: usize,
#[arg(short, long, default_value = "200")]
max: usize,
#[arg(long)]
context_only: bool,
#[arg(short = 't', long)]
file_type: Option<String>,
#[arg(short = 'n', long)]
line_numbers: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
extra_args: Vec<String>,
},
Init {
#[arg(short, long)]
global: bool,
#[arg(long)]
opencode: bool,
#[arg(long)]
gemini: bool,
#[arg(long, value_enum)]
agent: Option<AgentTarget>,
#[arg(long)]
show: bool,
#[arg(long = "claude-md", group = "mode")]
claude_md: bool,
#[arg(long = "hook-only", group = "mode")]
hook_only: bool,
#[arg(long = "auto-patch", group = "patch")]
auto_patch: bool,
#[arg(long = "no-patch", group = "patch")]
no_patch: bool,
#[arg(long)]
uninstall: bool,
#[arg(long)]
codex: bool,
#[arg(long)]
copilot: bool,
#[arg(long = "dry-run", conflicts_with = "show")]
dry_run: bool,
},
Wget {
url: String,
#[arg(short = 'O', long = "output-document", allow_hyphen_values = true)]
output: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Wc {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Gain {
#[arg(short, long)]
project: bool,
#[arg(short, long)]
graph: bool,
#[arg(short = 'H', long)]
history: bool,
#[arg(short, long)]
quota: bool,
#[arg(short, long, default_value = "20x", requires = "quota")]
tier: String,
#[arg(short, long)]
daily: bool,
#[arg(short, long)]
weekly: bool,
#[arg(short, long)]
monthly: bool,
#[arg(short, long)]
all: bool,
#[arg(short, long, default_value = "text")]
format: String,
#[arg(short = 'F', long)]
failures: bool,
#[arg(long)]
reset: bool,
#[arg(long, requires = "reset")]
yes: bool,
},
CcEconomics {
#[arg(short, long)]
daily: bool,
#[arg(short, long)]
weekly: bool,
#[arg(short, long)]
monthly: bool,
#[arg(short, long)]
all: bool,
#[arg(short, long, default_value = "text")]
format: String,
},
Config {
#[arg(long)]
create: bool,
},
Jest {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Vitest {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Prisma {
#[command(subcommand)]
command: PrismaCommands,
},
Tsc {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Next {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Lint {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Prettier {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Format {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Playwright {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Cargo {
#[command(subcommand)]
command: CargoCommands,
},
Npm {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Npx {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Curl {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Discover {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long, default_value = "15")]
limit: usize,
#[arg(short, long)]
all: bool,
#[arg(short, long, default_value = "30")]
since: u64,
#[arg(short, long, default_value = "text")]
format: String,
},
Session {},
Telemetry {
#[command(subcommand)]
command: core::telemetry_cmd::TelemetrySubcommand,
},
Learn {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(short, long, default_value = "30")]
since: u64,
#[arg(short, long, default_value = "text")]
format: String,
#[arg(short, long)]
write_rules: bool,
#[arg(long, default_value = "0.6")]
min_confidence: f64,
#[arg(long, default_value = "1")]
min_occurrences: usize,
},
Run {
#[arg(short = 'c', long = "command")]
command: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Proxy {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<OsString>,
},
Pipe {
#[arg(short, long)]
filter: Option<String>,
#[arg(long)]
passthrough: bool,
},
Trust {
#[arg(long)]
list: bool,
},
Untrust,
Verify {
#[arg(long)]
filter: Option<String>,
#[arg(long)]
require_all: bool,
},
Ruff {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pytest {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Mypy {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Rake {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Rubocop {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Rspec {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pip {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Go {
#[command(subcommand)]
command: GoCommands,
},
Gt {
#[command(subcommand)]
command: GtCommands,
},
#[command(name = "golangci-lint")]
GolangciLint {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(name = "gradlew")]
Gradlew {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(name = "hook-audit")]
HookAudit {
#[arg(short, long, default_value = "7")]
since: u64,
},
Rewrite {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Hook {
#[command(subcommand)]
command: HookCommands,
},
}
#[derive(Debug, Subcommand)]
enum HookCommands {
Claude,
Cursor,
Gemini,
Copilot,
Check {
#[arg(long, default_value = "claude")]
agent: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
}
#[derive(Debug, Subcommand)]
enum GitCommands {
Diff {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Log {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Status {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Show {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Add {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Commit {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Push {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pull {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Branch {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Fetch {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Stash {
subcommand: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Worktree {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum PnpmCommands {
List {
#[arg(short, long, default_value = "0")]
depth: usize,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Outdated {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Install {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Typecheck {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum DockerCommands {
Ps {
#[arg(short = 'a', long)]
all: bool,
},
Images,
Logs { container: String },
Compose {
#[command(subcommand)]
command: ComposeCommands,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum ComposeCommands {
Ps {
#[arg(short = 'a', long)]
all: bool,
},
Logs {
service: Option<String>,
#[arg(long, default_value_t = 100)]
tail: u32,
},
Build {
service: Option<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum KubectlCommands {
Get {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pods {
#[arg(short, long)]
namespace: Option<String>,
#[arg(short = 'A', long)]
all: bool,
},
Services {
#[arg(short, long)]
namespace: Option<String>,
#[arg(short = 'A', long)]
all: bool,
},
Logs {
pod: String,
#[arg(short, long)]
container: Option<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum PrismaCommands {
Generate {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Migrate {
#[command(subcommand)]
command: PrismaMigrateCommands,
},
DbPush {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
#[derive(Debug, Subcommand)]
enum PrismaMigrateCommands {
Dev {
#[arg(short, long)]
name: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Status {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Deploy {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
#[derive(Debug, Subcommand)]
enum CargoCommands {
Build {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Test {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Clippy {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Check {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Install {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Nextest {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum DotnetCommands {
Build {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Test {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Restore {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Format {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
#[derive(Debug, Subcommand)]
enum GoCommands {
Test {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Build {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Vet {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
const RTK_META_COMMANDS: &[&str] = &[
"gain",
"discover",
"learn",
"init",
"config",
"proxy",
"run",
"hook",
"hook-audit",
"pipe",
"cc-economics",
"verify",
"trust",
"untrust",
"session",
"rewrite",
];
fn run_fallback(parse_error: clap::Error) -> Result<i32> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() {
parse_error.exit();
}
if RTK_META_COMMANDS.contains(&args[0].as_str()) {
parse_error.exit();
}
let raw_command = args.join(" ");
let error_message = core::utils::strip_ansi(&parse_error.to_string());
let timer = core::tracking::TimedExecution::start();
let lookup_cmd = {
let base = std::path::Path::new(&args[0])
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| args[0].clone());
std::iter::once(base.as_str())
.chain(args[1..].iter().map(|s| s.as_str()))
.collect::<Vec<_>>()
.join(" ")
};
let toml_match = if std::env::var("RTK_NO_TOML").ok().as_deref() == Some("1") {
None
} else {
core::toml_filter::find_matching_filter(&lookup_cmd)
};
if let Some(filter) = toml_match {
let result = if filter.filter_stderr {
core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
} else {
core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.output()
};
match result {
Ok(output) => {
let exit_code = core::utils::exit_code_from_output(&output, &raw_command);
let stdout_raw = String::from_utf8_lossy(&output.stdout);
let stderr_raw = String::from_utf8_lossy(&output.stderr);
let combined_raw = if filter.filter_stderr {
format!("{}{}", stdout_raw, stderr_raw)
} else {
stdout_raw.to_string()
};
let tee_hint = if !output.status.success() {
core::tee::tee_and_hint(&combined_raw, &raw_command, exit_code)
} else {
None
};
let filtered = core::toml_filter::apply_filter(filter, &combined_raw);
println!("{}", filtered);
if let Some(hint) = tee_hint {
println!("{}", hint);
}
timer.track(
&raw_command,
&format!("rtk:toml {}", raw_command),
&combined_raw,
&filtered,
);
core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);
Ok(exit_code)
}
Err(e) => {
core::tracking::record_parse_failure_silent(&raw_command, &error_message, false);
eprintln!("[rtk: {}]", e);
Ok(127)
}
}
} else {
let status = core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
match status {
Ok(s) => {
timer.track_passthrough(&raw_command, &format!("rtk fallback: {}", raw_command));
core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);
Ok(core::utils::exit_code_from_status(&s, &raw_command))
}
Err(e) => {
core::tracking::record_parse_failure_silent(&raw_command, &error_message, false);
eprintln!("[rtk: {}]", e);
Ok(127)
}
}
}
}
#[derive(Debug, Subcommand)]
enum GtCommands {
Log {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Submit {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Sync {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Restack {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Create {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Branch {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(external_subcommand)]
Other(Vec<OsString>),
}
fn shell_split(input: &str) -> Vec<String> {
discover::lexer::shell_split(input)
}
fn merge_pnpm_args(filters: &[String], args: &[String]) -> Vec<String> {
filters
.iter()
.map(|filter| format!("--filter={}", filter))
.chain(args.iter().cloned())
.collect()
}
fn merge_pnpm_args_os(filters: &[String], args: &[OsString]) -> Vec<OsString> {
filters
.iter()
.map(|filter| OsString::from(format!("--filter={}", filter)))
.chain(args.iter().cloned())
.collect()
}
fn validate_pnpm_filters(filters: &[String], command: &PnpmCommands) -> Option<String> {
match command {
PnpmCommands::Typecheck { .. } => {
if !filters.is_empty() {
let cmd_name = match command {
PnpmCommands::Typecheck { .. } => "tsc",
_ => unreachable!(),
};
let msg = format!(
"[rtk] warning: --filter is not yet supported for pnpm {}, filters preceding the subcommand will be ignored",
cmd_name
);
return Some(msg);
}
None
}
_ => None,
}
}
fn main() {
let code = match run_cli() {
Ok(code) => code,
Err(e) => {
eprintln!("rtk: {:#}", e);
1
}
};
std::process::exit(code);
}
fn uninstall_init_dispatch<UninstallHermes, UninstallStandard>(
agent: Option<AgentTarget>,
global: bool,
gemini: bool,
codex: bool,
ctx: hooks::init::InitContext,
uninstall_hermes: UninstallHermes,
uninstall_standard: UninstallStandard,
) -> Result<()>
where
UninstallHermes: FnOnce(hooks::init::InitContext) -> Result<()>,
UninstallStandard: FnOnce(bool, bool, bool, bool, hooks::init::InitContext) -> Result<()>,
{
if agent == Some(AgentTarget::Hermes) {
uninstall_hermes(ctx)
} else {
let cursor = agent == Some(AgentTarget::Cursor);
uninstall_standard(global, gemini, codex, cursor, ctx)
}
}
fn run_cli() -> Result<i32> {
core::telemetry::maybe_ping();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
e.exit();
}
return run_fallback(e);
}
};
if !matches!(cli.command, Commands::Gain { .. }) {
hooks::hook_check::maybe_warn();
}
if is_operational_command(&cli.command) {
hooks::integrity::runtime_check()?;
}
let code = match cli.command {
Commands::Ls { args } => ls::run(&args, cli.verbose)?,
Commands::Tree { args } => tree::run(&args, cli.verbose)?,
Commands::Read {
files,
level,
max_lines,
tail_lines,
line_numbers,
} => {
let mut had_error = false;
let mut stdin_seen = false;
for file in &files {
let result = if file == Path::new("-") {
if stdin_seen {
eprintln!("rtk: warning: stdin specified more than once");
continue;
}
stdin_seen = true;
read::run_stdin(level, max_lines, tail_lines, line_numbers, cli.verbose)
} else {
read::run(
file,
level,
max_lines,
tail_lines,
line_numbers,
cli.verbose,
)
};
if let Err(e) = result {
eprintln!("cat: {}: {}", file.display(), e.root_cause());
had_error = true;
}
}
if had_error {
1
} else {
0
}
}
Commands::Smart {
file,
model,
force_download,
} => {
local_llm::run(&file, &model, force_download, cli.verbose)?;
0
}
Commands::Git {
directory,
config_override,
git_dir,
work_tree,
no_pager,
no_optional_locks,
bare,
literal_pathspecs,
command,
} => {
let mut global_args: Vec<String> = Vec::new();
for dir in &directory {
global_args.push("-C".to_string());
global_args.push(dir.clone());
}
for cfg in &config_override {
global_args.push("-c".to_string());
global_args.push(cfg.clone());
}
if let Some(ref dir) = git_dir {
global_args.push("--git-dir".to_string());
global_args.push(dir.clone());
}
if let Some(ref tree) = work_tree {
global_args.push("--work-tree".to_string());
global_args.push(tree.clone());
}
if no_pager {
global_args.push("--no-pager".to_string());
}
if no_optional_locks {
global_args.push("--no-optional-locks".to_string());
}
if bare {
global_args.push("--bare".to_string());
}
if literal_pathspecs {
global_args.push("--literal-pathspecs".to_string());
}
match command {
GitCommands::Diff { args } => git::run(
git::GitCommand::Diff,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Log { args } => {
git::run(git::GitCommand::Log, &args, None, cli.verbose, &global_args)?
}
GitCommands::Status { args } => git::run(
git::GitCommand::Status,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Show { args } => git::run(
git::GitCommand::Show,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Add { args } => {
git::run(git::GitCommand::Add, &args, None, cli.verbose, &global_args)?
}
GitCommands::Commit { args } => git::run(
git::GitCommand::Commit,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Push { args } => git::run(
git::GitCommand::Push,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Pull { args } => git::run(
git::GitCommand::Pull,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Branch { args } => git::run(
git::GitCommand::Branch,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Fetch { args } => git::run(
git::GitCommand::Fetch,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Stash { subcommand, args } => git::run(
git::GitCommand::Stash { subcommand },
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Worktree { args } => git::run(
git::GitCommand::Worktree,
&args,
None,
cli.verbose,
&global_args,
)?,
GitCommands::Other(args) => git::run_passthrough(&args, &global_args, cli.verbose)?,
}
}
Commands::Gh { subcommand, args } => {
gh_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?
}
Commands::Glab {
repo,
group,
subcommand,
mut args,
} => {
if let Some(r) = repo {
args.push("-R".to_string());
args.push(r);
}
if let Some(g) = group {
args.push("-g".to_string());
args.push(g);
}
glab_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?
}
Commands::Aws { subcommand, args } => aws_cmd::run(&subcommand, &args, cli.verbose)?,
Commands::Psql { args } => psql_cmd::run(&args, cli.verbose)?,
Commands::Pnpm { filter, command } => {
if let Some(warning) = validate_pnpm_filters(&filter, &command) {
eprintln!("{}", warning);
}
match command {
PnpmCommands::List { depth, args } => pnpm_cmd::run(
pnpm_cmd::PnpmCommand::List { depth },
&merge_pnpm_args(&filter, &args),
cli.verbose,
)?,
PnpmCommands::Outdated { args } => pnpm_cmd::run(
pnpm_cmd::PnpmCommand::Outdated,
&merge_pnpm_args(&filter, &args),
cli.verbose,
)?,
PnpmCommands::Install { args } => pnpm_cmd::run(
pnpm_cmd::PnpmCommand::Install,
&merge_pnpm_args(&filter, &args),
cli.verbose,
)?,
PnpmCommands::Typecheck { args } => tsc_cmd::run(&args, cli.verbose)?,
PnpmCommands::Other(args) => {
pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &args), cli.verbose)?
}
}
}
Commands::Err { command } => {
let cmd = command.join(" ");
runner::run_err(&cmd, cli.verbose)?
}
Commands::Test { command } => {
let cmd = command.join(" ");
runner::run_test(&cmd, cli.verbose)?
}
Commands::Json {
file,
depth,
keys_only,
} => {
if file == Path::new("-") {
json_cmd::run_stdin(depth, keys_only, cli.verbose)?;
} else {
json_cmd::run(&file, depth, keys_only, cli.verbose)?;
}
0
}
Commands::Deps { path } => {
deps::run(&path, cli.verbose)?;
0
}
Commands::Env { filter, show_all } => {
env_cmd::run(filter.as_deref(), show_all, cli.verbose)?;
0
}
Commands::Find { args } => {
find_cmd::run_from_args(&args, cli.verbose)?;
0
}
Commands::Diff { file1, file2 } => {
if let Some(f2) = file2 {
diff_cmd::run(&file1, &f2, cli.verbose)?;
} else {
diff_cmd::run_stdin(cli.verbose)?;
}
0
}
Commands::Log { file } => {
if let Some(f) = file {
log_cmd::run_file(&f, cli.verbose)?;
} else {
log_cmd::run_stdin(cli.verbose)?;
}
0
}
Commands::Dotnet { command } => match command {
DotnetCommands::Build { args } => dotnet_cmd::run_build(&args, cli.verbose)?,
DotnetCommands::Test { args } => dotnet_cmd::run_test(&args, cli.verbose)?,
DotnetCommands::Restore { args } => dotnet_cmd::run_restore(&args, cli.verbose)?,
DotnetCommands::Format { args } => dotnet_cmd::run_format(&args, cli.verbose)?,
DotnetCommands::Other(args) => dotnet_cmd::run_passthrough(&args, cli.verbose)?,
},
Commands::Docker { command } => match command {
DockerCommands::Ps { all } => {
let cmd = if all {
container::ContainerCmd::DockerPsAll
} else {
container::ContainerCmd::DockerPs
};
container::run(cmd, &[], cli.verbose)?
}
DockerCommands::Images => {
container::run(container::ContainerCmd::DockerImages, &[], cli.verbose)?
}
DockerCommands::Logs { container: c } => {
container::run(container::ContainerCmd::DockerLogs, &[c], cli.verbose)?
}
DockerCommands::Compose { command: compose } => match compose {
ComposeCommands::Ps { all } => container::run_compose_ps(all, cli.verbose)?,
ComposeCommands::Logs { service, tail } => {
container::run_compose_logs(service.as_deref(), tail, cli.verbose)?
}
ComposeCommands::Build { service } => {
container::run_compose_build(service.as_deref(), cli.verbose)?
}
ComposeCommands::Other(args) => {
container::run_compose_passthrough(&args, cli.verbose)?
}
},
DockerCommands::Other(args) => container::run_docker_passthrough(&args, cli.verbose)?,
},
Commands::Kubectl { command } => match command {
KubectlCommands::Get { args } => container::run_kubectl_get(&args, cli.verbose)?,
KubectlCommands::Pods { namespace, all } => {
let mut args: Vec<String> = Vec::new();
if all {
args.push("-A".to_string());
} else if let Some(n) = namespace {
args.push("-n".to_string());
args.push(n);
}
container::run(container::ContainerCmd::KubectlPods, &args, cli.verbose)?
}
KubectlCommands::Services { namespace, all } => {
let mut args: Vec<String> = Vec::new();
if all {
args.push("-A".to_string());
} else if let Some(n) = namespace {
args.push("-n".to_string());
args.push(n);
}
container::run(container::ContainerCmd::KubectlServices, &args, cli.verbose)?
}
KubectlCommands::Logs { pod, container: c } => {
let mut args = vec![pod];
if let Some(cont) = c {
args.push("-c".to_string());
args.push(cont);
}
container::run(container::ContainerCmd::KubectlLogs, &args, cli.verbose)?
}
KubectlCommands::Other(args) => container::run_kubectl_passthrough(&args, cli.verbose)?,
},
Commands::Summary { command } => {
let cmd = command.join(" ");
summary::run(&cmd, cli.verbose)?
}
Commands::Grep {
pattern,
path,
max_len,
max,
context_only,
file_type,
line_numbers: _,
extra_args,
} => grep_cmd::run(
&pattern,
&path,
max_len,
max,
context_only,
file_type.as_deref(),
&extra_args,
cli.verbose,
)?,
Commands::Init {
global,
opencode,
gemini,
agent,
show,
claude_md,
hook_only,
auto_patch,
no_patch,
uninstall,
codex,
copilot,
dry_run,
} => {
let ctx = hooks::init::InitContext {
verbose: cli.verbose,
dry_run,
};
if show {
hooks::init::show_config(codex)?;
} else if uninstall {
uninstall_init_dispatch(
agent,
global,
gemini,
codex,
ctx,
hooks::init::uninstall_hermes,
hooks::init::uninstall,
)?;
} else if gemini {
let patch_mode = if auto_patch {
hooks::init::PatchMode::Auto
} else if no_patch {
hooks::init::PatchMode::Skip
} else {
hooks::init::PatchMode::Ask
};
hooks::init::run_gemini(global, hook_only, patch_mode, ctx)?;
} else if copilot {
hooks::init::run_copilot(ctx)?;
} else if agent == Some(AgentTarget::Kilocode) {
if global {
anyhow::bail!("Kilo Code is project-scoped. Use: rtk init --agent kilocode");
}
hooks::init::run_kilocode_mode(ctx)?;
} else if agent == Some(AgentTarget::Antigravity) {
if global {
anyhow::bail!(
"Antigravity is project-scoped. Use: rtk init --agent antigravity"
);
}
hooks::init::run_antigravity_mode(ctx)?;
} else if agent == Some(AgentTarget::Hermes) {
hooks::init::run_hermes_mode(ctx)?;
} else {
let install_opencode = opencode;
let install_claude = !opencode;
let install_cursor = agent == Some(AgentTarget::Cursor);
let install_windsurf = agent == Some(AgentTarget::Windsurf);
let install_cline = agent == Some(AgentTarget::Cline);
let patch_mode = if auto_patch {
hooks::init::PatchMode::Auto
} else if no_patch {
hooks::init::PatchMode::Skip
} else {
hooks::init::PatchMode::Ask
};
hooks::init::run(
global,
install_claude,
install_opencode,
install_cursor,
install_windsurf,
install_cline,
claude_md,
hook_only,
codex,
patch_mode,
ctx,
)?;
}
0
}
Commands::Wget { url, output, args } => {
if output.as_deref() == Some("-") {
wget_cmd::run_stdout(&url, &args, cli.verbose)?
} else {
let mut all_args = Vec::new();
if let Some(out_file) = &output {
all_args.push("-O".to_string());
all_args.push(out_file.clone());
}
all_args.extend(args);
wget_cmd::run(&url, &all_args, cli.verbose)?
}
}
Commands::Wc { args } => wc_cmd::run(&args, cli.verbose)?,
Commands::Gain {
project,
graph,
history,
quota,
tier,
daily,
weekly,
monthly,
all,
format,
failures,
reset,
yes,
} => {
analytics::gain::run(
project,
graph,
history,
quota,
&tier,
daily,
weekly,
monthly,
all,
&format,
failures,
reset,
yes,
cli.verbose,
)?;
0
}
Commands::CcEconomics {
daily,
weekly,
monthly,
all,
format,
} => {
analytics::cc_economics::run(daily, weekly, monthly, all, &format, cli.verbose)?;
0
}
Commands::Config { create } => {
if create {
let path = core::config::Config::create_default()?;
println!("Created: {}", path.display());
} else {
core::config::show_config()?;
}
0
}
Commands::Jest { ref args } | Commands::Vitest { ref args } => {
vitest_cmd::run_test(&cli.command, args, cli.verbose)?
}
Commands::Prisma { command } => match command {
PrismaCommands::Generate { args } => {
prisma_cmd::run(prisma_cmd::PrismaCommand::Generate, &args, cli.verbose)?
}
PrismaCommands::Migrate { command } => match command {
PrismaMigrateCommands::Dev { name, args } => prisma_cmd::run(
prisma_cmd::PrismaCommand::Migrate {
subcommand: prisma_cmd::MigrateSubcommand::Dev { name },
},
&args,
cli.verbose,
)?,
PrismaMigrateCommands::Status { args } => prisma_cmd::run(
prisma_cmd::PrismaCommand::Migrate {
subcommand: prisma_cmd::MigrateSubcommand::Status,
},
&args,
cli.verbose,
)?,
PrismaMigrateCommands::Deploy { args } => prisma_cmd::run(
prisma_cmd::PrismaCommand::Migrate {
subcommand: prisma_cmd::MigrateSubcommand::Deploy,
},
&args,
cli.verbose,
)?,
},
PrismaCommands::DbPush { args } => {
prisma_cmd::run(prisma_cmd::PrismaCommand::DbPush, &args, cli.verbose)?
}
},
Commands::Tsc { args } => tsc_cmd::run(&args, cli.verbose)?,
Commands::Next { args } => next_cmd::run(&args, cli.verbose)?,
Commands::Lint { args } => lint_cmd::run(&args, cli.verbose)?,
Commands::Prettier { args } => prettier_cmd::run(&args, cli.verbose)?,
Commands::Format { args } => format_cmd::run(&args, cli.verbose)?,
Commands::Playwright { args } => playwright_cmd::run(&args, cli.verbose)?,
Commands::Cargo { command } => match command {
CargoCommands::Build { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Build, &args, cli.verbose)?
}
CargoCommands::Test { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Test, &args, cli.verbose)?
}
CargoCommands::Clippy { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Clippy, &args, cli.verbose)?
}
CargoCommands::Check { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Check, &args, cli.verbose)?
}
CargoCommands::Install { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Install, &args, cli.verbose)?
}
CargoCommands::Nextest { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Nextest, &args, cli.verbose)?
}
CargoCommands::Other(args) => cargo_cmd::run_passthrough(&args, cli.verbose)?,
},
Commands::Npm { args } => npm_cmd::run(&args, cli.verbose, cli.skip_env)?,
Commands::Curl { args } => curl_cmd::run(&args, cli.verbose)?,
Commands::Discover {
project,
limit,
all,
since,
format,
} => {
discover::run(project.as_deref(), all, since, limit, &format, cli.verbose)?;
0
}
Commands::Session {} => {
analytics::session_cmd::run(cli.verbose)?;
0
}
Commands::Telemetry { command } => {
core::telemetry_cmd::run(&command)?;
0
}
Commands::Learn {
project,
all,
since,
format,
write_rules,
min_confidence,
min_occurrences,
} => {
learn::run(
project,
all,
since,
format,
write_rules,
min_confidence,
min_occurrences,
)?;
0
}
Commands::Npx { args } => {
if args.is_empty() {
anyhow::bail!("npx requires a command argument");
}
match args[0].as_str() {
"tsc" | "typescript" => tsc_cmd::run(&args[1..], cli.verbose)?,
"eslint" => lint_cmd::run(&args[1..], cli.verbose)?,
"prisma" => {
if args.len() > 1 {
let prisma_args: Vec<String> = args[2..].to_vec();
match args[1].as_str() {
"generate" => prisma_cmd::run(
prisma_cmd::PrismaCommand::Generate,
&prisma_args,
cli.verbose,
)?,
"db" if args.len() > 2 && args[2] == "push" => prisma_cmd::run(
prisma_cmd::PrismaCommand::DbPush,
&args[3..],
cli.verbose,
)?,
_ => {
let timer = core::tracking::TimedExecution::start();
let mut cmd = core::utils::resolved_command("npx");
for arg in &args {
cmd.arg(arg);
}
let status = cmd.status().context("Failed to run npx prisma")?;
let args_str = args.join(" ");
timer.track_passthrough(
&format!("npx {}", args_str),
&format!("rtk npx {} (passthrough)", args_str),
);
core::utils::exit_code_from_status(&status, "npx prisma")
}
}
} else {
let timer = core::tracking::TimedExecution::start();
let status = core::utils::resolved_command("npx")
.arg("prisma")
.status()
.context("Failed to run npx prisma")?;
timer.track_passthrough("npx prisma", "rtk npx prisma (passthrough)");
core::utils::exit_code_from_status(&status, "npx prisma")
}
}
"next" => next_cmd::run(&args[1..], cli.verbose)?,
"prettier" => prettier_cmd::run(&args[1..], cli.verbose)?,
"playwright" => playwright_cmd::run(&args[1..], cli.verbose)?,
_ => npm_cmd::exec(&args, cli.verbose, cli.skip_env)?,
}
}
Commands::Ruff { args } => ruff_cmd::run(&args, cli.verbose)?,
Commands::Pytest { args } => pytest_cmd::run(&args, cli.verbose)?,
Commands::Mypy { args } => mypy_cmd::run(&args, cli.verbose)?,
Commands::Rake { args } => rake_cmd::run(&args, cli.verbose)?,
Commands::Rubocop { args } => rubocop_cmd::run(&args, cli.verbose)?,
Commands::Rspec { args } => rspec_cmd::run(&args, cli.verbose)?,
Commands::Pip { args } => pip_cmd::run(&args, cli.verbose)?,
Commands::Go { command } => match command {
GoCommands::Test { args } => go_cmd::run_test(&args, cli.verbose)?,
GoCommands::Build { args } => go_cmd::run_build(&args, cli.verbose)?,
GoCommands::Vet { args } => go_cmd::run_vet(&args, cli.verbose)?,
GoCommands::Other(args) => go_cmd::run_other(&args, cli.verbose)?,
},
Commands::Gt { command } => match command {
GtCommands::Log { args } => gt_cmd::run_log(&args, cli.verbose)?,
GtCommands::Submit { args } => gt_cmd::run_submit(&args, cli.verbose)?,
GtCommands::Sync { args } => gt_cmd::run_sync(&args, cli.verbose)?,
GtCommands::Restack { args } => gt_cmd::run_restack(&args, cli.verbose)?,
GtCommands::Create { args } => gt_cmd::run_create(&args, cli.verbose)?,
GtCommands::Branch { args } => gt_cmd::run_branch(&args, cli.verbose)?,
GtCommands::Other(args) => gt_cmd::run_other(&args, cli.verbose)?,
},
Commands::GolangciLint { args } => golangci_cmd::run(&args, cli.verbose)?,
Commands::Gradlew { args } => gradlew_cmd::run(&args, cli.verbose)?,
Commands::HookAudit { since } => {
hooks::hook_audit_cmd::run(since, cli.verbose)?;
0
}
Commands::Hook { command } => match command {
HookCommands::Claude => {
hooks::hook_cmd::run_claude()?;
0
}
HookCommands::Cursor => {
hooks::hook_cmd::run_cursor()?;
0
}
HookCommands::Gemini => {
hooks::hook_cmd::run_gemini()?;
0
}
HookCommands::Copilot => {
hooks::hook_cmd::run_copilot()?;
0
}
HookCommands::Check { agent: _, command } => {
use crate::discover::registry::rewrite_command;
let raw = command.join(" ");
let (excluded, transparent_prefixes) = crate::core::config::Config::load()
.map(|c| (c.hooks.exclude_commands, c.hooks.transparent_prefixes))
.unwrap_or_default();
match rewrite_command(&raw, &excluded, &transparent_prefixes) {
Some(rewritten) => {
println!("{}", rewritten);
0
}
None => {
eprintln!("No rewrite for: {}", raw);
1
}
}
}
},
Commands::Rewrite { args } => {
let cmd = args.join(" ");
hooks::rewrite_cmd::run(&cmd)?;
0
}
Commands::Pipe {
filter,
passthrough,
} => {
pipe_cmd::run(filter.as_deref(), passthrough)?;
0
}
Commands::Run { command, args } => {
let raw = match command {
Some(c) => c,
None if !args.is_empty() => args.join(" "),
None => String::new(),
};
if raw.trim().is_empty() {
0
} else {
use std::process::Command as ProcCommand;
let shell = if cfg!(windows) { "cmd" } else { "sh" };
let flag = if cfg!(windows) { "/C" } else { "-c" };
let status = ProcCommand::new(shell)
.arg(flag)
.arg(&raw)
.status()
.with_context(|| format!("Failed to execute: {}", raw))?;
status.code().unwrap_or(1)
}
}
Commands::Proxy { args } => {
use std::io::{Read, Write};
use std::process::Stdio;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
if args.is_empty() {
anyhow::bail!(
"proxy requires a command to execute\nUsage: rtk proxy <command> [args...]"
);
}
let timer = core::tracking::TimedExecution::start();
let (cmd_name, cmd_args): (String, Vec<String>) = if args.len() == 1 {
let full = args[0].to_string_lossy();
let parts = shell_split(&full);
if parts.len() > 1 {
(parts[0].clone(), parts[1..].to_vec())
} else {
(full.into_owned(), vec![])
}
} else {
(
args[0].to_string_lossy().into_owned(),
args[1..]
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect(),
)
};
if cli.verbose > 0 {
eprintln!("Proxy mode: {} {}", cmd_name, cmd_args.join(" "));
}
static PROXY_CHILD_PID: AtomicU32 = AtomicU32::new(0);
#[cfg(unix)]
#[allow(unsafe_code)]
{
unsafe extern "C" fn handle_signal(sig: libc::c_int) {
let pid = PROXY_CHILD_PID.load(Ordering::SeqCst);
if pid != 0 {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
libc::waitpid(pid as libc::pid_t, std::ptr::null_mut(), 0);
}
libc::signal(sig, libc::SIG_DFL);
libc::raise(sig);
}
unsafe {
libc::signal(
libc::SIGINT,
handle_signal as *const () as libc::sighandler_t,
);
libc::signal(
libc::SIGTERM,
handle_signal as *const () as libc::sighandler_t,
);
}
}
struct ChildGuard(Option<std::process::Child>);
impl Drop for ChildGuard {
fn drop(&mut self) {
if let Some(mut child) = self.0.take() {
let _ = child.kill();
let _ = child.wait();
}
PROXY_CHILD_PID.store(0, Ordering::SeqCst);
}
}
let mut child = ChildGuard(Some(
core::utils::resolved_command(cmd_name.as_ref())
.args(&cmd_args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context(format!("Failed to execute command: {}", cmd_name))?,
));
if let Some(ref inner) = child.0 {
PROXY_CHILD_PID.store(inner.id(), Ordering::SeqCst);
}
let inner = child.0.as_mut().context("Child process missing")?;
let stdout_pipe = inner
.stdout
.take()
.context("Failed to capture child stdout")?;
let stderr_pipe = inner
.stderr
.take()
.context("Failed to capture child stderr")?;
const CAP: usize = 1_048_576;
let stdout_handle = thread::spawn(move || -> std::io::Result<Vec<u8>> {
let mut reader = stdout_pipe;
let mut captured = Vec::new();
let mut buf = [0u8; 8192];
loop {
let count = reader.read(&mut buf)?;
if count == 0 {
break;
}
if captured.len() < CAP {
let take = count.min(CAP - captured.len());
captured.extend_from_slice(&buf[..take]);
}
let mut out = std::io::stdout().lock();
out.write_all(&buf[..count])?;
out.flush()?;
}
Ok(captured)
});
let stderr_handle = thread::spawn(move || -> std::io::Result<Vec<u8>> {
let mut reader = stderr_pipe;
let mut captured = Vec::new();
let mut buf = [0u8; 8192];
loop {
let count = reader.read(&mut buf)?;
if count == 0 {
break;
}
if captured.len() < CAP {
let take = count.min(CAP - captured.len());
captured.extend_from_slice(&buf[..take]);
}
let mut err = std::io::stderr().lock();
err.write_all(&buf[..count])?;
err.flush()?;
}
Ok(captured)
});
let status = child
.0
.take()
.context("Child process missing")?
.wait()
.context(format!("Failed waiting for command: {}", cmd_name))?;
let stdout_bytes = stdout_handle
.join()
.map_err(|_| anyhow::anyhow!("stdout streaming thread panicked"))??;
let stderr_bytes = stderr_handle
.join()
.map_err(|_| anyhow::anyhow!("stderr streaming thread panicked"))??;
let stdout = String::from_utf8_lossy(&stdout_bytes);
let stderr = String::from_utf8_lossy(&stderr_bytes);
let full_output = format!("{}{}", stdout, stderr);
timer.track(
&format!("{} {}", cmd_name, cmd_args.join(" ")),
&format!("rtk proxy {} {}", cmd_name, cmd_args.join(" ")),
&full_output,
&full_output,
);
core::utils::exit_code_from_status(&status, &cmd_name)
}
Commands::Trust { list } => {
hooks::trust::run_trust(list)?;
0
}
Commands::Untrust => {
hooks::trust::run_untrust()?;
0
}
Commands::Verify {
filter,
require_all,
} => {
if filter.is_some() {
hooks::verify_cmd::run(filter, require_all)?;
} else {
hooks::integrity::run_verify(cli.verbose)?;
hooks::verify_cmd::run(None, require_all)?;
}
0
}
};
Ok(code)
}
fn is_operational_command(cmd: &Commands) -> bool {
matches!(
cmd,
Commands::Ls { .. }
| Commands::Tree { .. }
| Commands::Read { .. }
| Commands::Smart { .. }
| Commands::Git { .. }
| Commands::Gh { .. }
| Commands::Glab { .. }
| Commands::Pnpm { .. }
| Commands::Err { .. }
| Commands::Test { .. }
| Commands::Json { .. }
| Commands::Deps { .. }
| Commands::Env { .. }
| Commands::Find { .. }
| Commands::Diff { .. }
| Commands::Log { .. }
| Commands::Dotnet { .. }
| Commands::Docker { .. }
| Commands::Kubectl { .. }
| Commands::Summary { .. }
| Commands::Grep { .. }
| Commands::Wget { .. }
| Commands::Vitest { .. }
| Commands::Prisma { .. }
| Commands::Tsc { .. }
| Commands::Next { .. }
| Commands::Lint { .. }
| Commands::Prettier { .. }
| Commands::Playwright { .. }
| Commands::Cargo { .. }
| Commands::Npm { .. }
| Commands::Npx { .. }
| Commands::Curl { .. }
| Commands::Ruff { .. }
| Commands::Pytest { .. }
| Commands::Rake { .. }
| Commands::Rubocop { .. }
| Commands::Rspec { .. }
| Commands::Pip { .. }
| Commands::Go { .. }
| Commands::GolangciLint { .. }
| Commands::Gt { .. }
)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use std::cell::Cell;
#[test]
fn test_git_commit_single_message() {
let cli = Cli::try_parse_from(["rtk", "git", "commit", "-m", "fix: typo"]).unwrap();
match cli.command {
Commands::Git {
command: GitCommands::Commit { args },
..
} => {
assert_eq!(args, vec!["-m", "fix: typo"]);
}
_ => panic!("Expected Git Commit command"),
}
}
#[test]
fn test_git_commit_multiple_messages() {
let cli = Cli::try_parse_from([
"rtk",
"git",
"commit",
"-m",
"feat: add support",
"-m",
"Body paragraph here.",
])
.unwrap();
match cli.command {
Commands::Git {
command: GitCommands::Commit { args },
..
} => {
assert_eq!(
args,
vec!["-m", "feat: add support", "-m", "Body paragraph here."]
);
}
_ => panic!("Expected Git Commit command"),
}
}
#[test]
fn test_git_commit_am_flag() {
let cli = Cli::try_parse_from(["rtk", "git", "commit", "-am", "quick fix"]).unwrap();
match cli.command {
Commands::Git {
command: GitCommands::Commit { args },
..
} => {
assert_eq!(args, vec!["-am", "quick fix"]);
}
_ => panic!("Expected Git Commit command"),
}
}
#[test]
fn test_git_commit_amend() {
let cli =
Cli::try_parse_from(["rtk", "git", "commit", "--amend", "-m", "new msg"]).unwrap();
match cli.command {
Commands::Git {
command: GitCommands::Commit { args },
..
} => {
assert_eq!(args, vec!["--amend", "-m", "new msg"]);
}
_ => panic!("Expected Git Commit command"),
}
}
#[test]
fn test_git_global_options_parsing() {
let cli =
Cli::try_parse_from(["rtk", "git", "--no-pager", "--no-optional-locks", "status"])
.unwrap();
match cli.command {
Commands::Git {
no_pager,
no_optional_locks,
bare,
literal_pathspecs,
..
} => {
assert!(no_pager);
assert!(no_optional_locks);
assert!(!bare);
assert!(!literal_pathspecs);
}
_ => panic!("Expected Git command"),
}
}
#[test]
fn test_git_commit_long_flag_multiple() {
let cli = Cli::try_parse_from([
"rtk",
"git",
"commit",
"--message",
"title",
"--message",
"body",
"--message",
"footer",
])
.unwrap();
match cli.command {
Commands::Git {
command: GitCommands::Commit { args },
..
} => {
assert_eq!(
args,
vec![
"--message",
"title",
"--message",
"body",
"--message",
"footer"
]
);
}
_ => panic!("Expected Git Commit command"),
}
}
#[test]
fn test_try_parse_valid_git_status() {
let result = Cli::try_parse_from(["rtk", "git", "status"]);
assert!(result.is_ok(), "git status should parse successfully");
}
#[test]
fn test_try_parse_init_agent_hermes() {
let cli = Cli::try_parse_from(["rtk", "init", "--agent", "hermes"]).unwrap();
match cli.command {
Commands::Init { agent, .. } => {
assert_eq!(agent, Some(AgentTarget::Hermes));
}
_ => panic!("Expected Init command"),
}
}
#[test]
fn test_try_parse_kubectl_get_alias() {
let cli = Cli::try_parse_from(["rtk", "kubectl", "get", "pods", "-n", "default"]).unwrap();
match cli.command {
Commands::Kubectl {
command: KubectlCommands::Get { args },
} => assert_eq!(args, vec!["pods", "-n", "default"]),
_ => panic!("Expected Kubectl Get command"),
}
}
#[test]
fn test_try_parse_init_agent_hermes_uninstall() {
let cli = Cli::try_parse_from(["rtk", "init", "--agent", "hermes", "--uninstall"]).unwrap();
match cli.command {
Commands::Init {
agent, uninstall, ..
} => {
assert_eq!(agent, Some(AgentTarget::Hermes));
assert!(uninstall);
}
_ => panic!("Expected Init command"),
}
}
#[test]
fn test_init_uninstall_dispatch_routes_hermes_to_hermes_cleanup() {
let hermes_called = Cell::new(false);
let standard_called = Cell::new(false);
let ctx = hooks::init::InitContext {
verbose: 2,
dry_run: true,
};
let result = uninstall_init_dispatch(
Some(AgentTarget::Hermes),
true,
false,
false,
ctx,
|ctx| {
hermes_called.set(true);
assert_eq!(ctx.verbose, 2);
assert!(ctx.dry_run);
Ok(())
},
|_, _, _, _, _| {
standard_called.set(true);
Ok(())
},
);
assert!(result.is_ok());
assert!(hermes_called.get());
assert!(!standard_called.get());
}
#[test]
fn test_try_parse_help_is_display_help() {
match Cli::try_parse_from(["rtk", "--help"]) {
Err(e) => assert_eq!(e.kind(), ErrorKind::DisplayHelp),
Ok(_) => panic!("Expected DisplayHelp error"),
}
}
#[test]
fn test_try_parse_version_is_display_version() {
match Cli::try_parse_from(["rtk", "--version"]) {
Err(e) => assert_eq!(e.kind(), ErrorKind::DisplayVersion),
Ok(_) => panic!("Expected DisplayVersion error"),
}
}
#[test]
fn test_try_parse_unknown_subcommand_is_error() {
match Cli::try_parse_from(["rtk", "nonexistent-command"]) {
Err(e) => assert!(!matches!(
e.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
)),
Ok(_) => panic!("Expected parse error for unknown subcommand"),
}
}
#[test]
fn test_try_parse_git_with_dash_c_succeeds() {
let result = Cli::try_parse_from(["rtk", "git", "-C", "/path", "status"]);
assert!(
result.is_ok(),
"git -C /path status should parse successfully"
);
if let Ok(cli) = result {
match cli.command {
Commands::Git { directory, .. } => {
assert_eq!(directory, vec!["/path"]);
}
_ => panic!("Expected Git command"),
}
}
}
#[test]
fn test_gain_failures_flag_parses() {
let result = Cli::try_parse_from(["rtk", "gain", "--failures"]);
assert!(result.is_ok());
if let Ok(cli) = result {
match cli.command {
Commands::Gain { failures, .. } => assert!(failures),
_ => panic!("Expected Gain command"),
}
}
}
#[test]
fn test_gain_failures_short_flag_parses() {
let result = Cli::try_parse_from(["rtk", "gain", "-F"]);
assert!(result.is_ok());
if let Ok(cli) = result {
match cli.command {
Commands::Gain { failures, .. } => assert!(failures),
_ => panic!("Expected Gain command"),
}
}
}
#[test]
fn test_meta_commands_reject_bad_flags() {
for cmd in RTK_META_COMMANDS {
if matches!(*cmd, "proxy" | "run" | "rewrite" | "session") {
continue;
}
let result = Cli::try_parse_from(["rtk", cmd, "--nonexistent-flag-xyz"]);
assert!(
result.is_err(),
"Meta-command '{}' with bad flag should fail to parse",
cmd
);
}
}
#[test]
fn test_run_command_with_dash_c() {
let cli = Cli::try_parse_from(["rtk", "run", "-c", "git status && echo done"]).unwrap();
match cli.command {
Commands::Run { command, args } => {
assert_eq!(command, Some("git status && echo done".to_string()));
assert!(args.is_empty());
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_run_command_positional_args() {
let cli = Cli::try_parse_from(["rtk", "run", "echo", "hello"]).unwrap();
match cli.command {
Commands::Run { command, args } => {
assert!(command.is_none());
assert_eq!(args, vec!["echo", "hello"]);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_hook_claude_parses() {
let cli = Cli::try_parse_from(["rtk", "hook", "claude"]).unwrap();
assert!(matches!(
cli.command,
Commands::Hook {
command: HookCommands::Claude
}
));
}
#[test]
fn test_hook_check_parses() {
let cli = Cli::try_parse_from(["rtk", "hook", "check", "git", "status"]).unwrap();
match cli.command {
Commands::Hook {
command: HookCommands::Check { agent, command },
} => {
assert_eq!(agent, "claude");
assert_eq!(command, vec!["git", "status"]);
}
_ => panic!("Expected Hook Check command"),
}
}
#[test]
fn test_hook_check_with_agent() {
let cli =
Cli::try_parse_from(["rtk", "hook", "check", "--agent", "gemini", "cargo", "test"])
.unwrap();
match cli.command {
Commands::Hook {
command: HookCommands::Check { agent, command },
} => {
assert_eq!(agent, "gemini");
assert_eq!(command, vec!["cargo", "test"]);
}
_ => panic!("Expected Hook Check command"),
}
}
#[test]
fn test_hook_check_preserves_double_dash_in_command() {
let cli = Cli::try_parse_from([
"rtk",
"hook",
"check",
"shadowenv",
"exec",
"--",
"git",
"status",
])
.unwrap();
match cli.command {
Commands::Hook {
command: HookCommands::Check { agent, command },
} => {
assert_eq!(agent, "claude");
assert_eq!(command, vec!["shadowenv", "exec", "--", "git", "status"]);
}
_ => panic!("Expected Hook Check command"),
}
}
#[test]
fn test_meta_command_list_is_complete() {
let meta_cmds_that_parse = [
vec!["rtk", "gain"],
vec!["rtk", "discover"],
vec!["rtk", "learn"],
vec!["rtk", "init"],
vec!["rtk", "config"],
vec!["rtk", "proxy", "echo", "hi"],
vec!["rtk", "run", "-c", "echo hi"],
vec!["rtk", "hook-audit"],
vec!["rtk", "cc-economics"],
];
for args in &meta_cmds_that_parse {
let result = Cli::try_parse_from(args.iter());
assert!(
result.is_ok(),
"Meta-command {:?} should parse successfully",
args
);
}
}
#[test]
fn test_shell_split_simple() {
assert_eq!(
shell_split("head -50 file.php"),
vec!["head", "-50", "file.php"]
);
}
#[test]
fn test_shell_split_double_quotes() {
assert_eq!(
shell_split(r#"git log --format="%H %s""#),
vec!["git", "log", "--format=%H %s"]
);
}
#[test]
fn test_shell_split_single_quotes() {
assert_eq!(
shell_split("grep -r 'hello world' ."),
vec!["grep", "-r", "hello world", "."]
);
}
#[test]
fn test_shell_split_single_word() {
assert_eq!(shell_split("ls"), vec!["ls"]);
}
#[test]
fn test_shell_split_empty() {
let result: Vec<String> = shell_split("");
assert!(result.is_empty());
}
#[test]
fn test_rewrite_clap_multi_args() {
let cases = vec![
vec!["rtk", "rewrite", "ls", "-al"],
vec!["rtk", "rewrite", "git", "status"],
vec!["rtk", "rewrite", "npm", "exec"],
vec!["rtk", "rewrite", "cargo", "test"],
vec!["rtk", "rewrite", "du", "-sh", "."],
vec!["rtk", "rewrite", "head", "-50", "file.txt"],
];
for args in &cases {
let result = Cli::try_parse_from(args.iter());
assert!(
result.is_ok(),
"rtk rewrite {:?} should parse (was failing before trailing_var_arg fix)",
&args[2..]
);
if let Ok(cli) = result {
match cli.command {
Commands::Rewrite { ref args } => {
assert!(args.len() >= 2, "rewrite args should capture all tokens");
}
_ => panic!("expected Rewrite command"),
}
}
}
}
#[test]
fn test_rewrite_clap_quoted_single_arg() {
let result = Cli::try_parse_from(["rtk", "rewrite", "git status"]);
assert!(result.is_ok());
if let Ok(cli) = result {
match cli.command {
Commands::Rewrite { ref args } => {
assert_eq!(args.len(), 1);
assert_eq!(args[0], "git status");
}
_ => panic!("expected Rewrite command"),
}
}
}
#[test]
fn test_merge_filters_with_no_args() {
let filters = vec![];
let args = vec!["--depth=0".to_string(), "--no-verbose".to_string()];
let expected_args = vec!["--depth=0", "--no-verbose"];
assert_eq!(merge_pnpm_args(&filters, &args), expected_args);
}
#[test]
fn test_merge_filters_with_args() {
let filters = vec!["@app1".to_string(), "@app2".to_string()];
let args = vec![
"--filter=@app3".to_string(),
"--depth=0".to_string(),
"--no-verbose".to_string(),
];
let expected_args = vec![
"--filter=@app1",
"--filter=@app2",
"--filter=@app3",
"--depth=0",
"--no-verbose",
];
assert_eq!(merge_pnpm_args(&filters, &args), expected_args);
}
#[test]
fn test_merge_filters_with_no_args_os() {
let filters = vec![];
let args = vec![OsString::from("--depth=0")];
let expected_args = vec![OsString::from("--depth=0")];
assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args);
}
#[test]
fn test_merge_filters_with_args_os() {
let filters = vec!["@app1".to_string()];
let args = vec![OsString::from("--depth=0")];
let expected_args = vec![
OsString::from("--filter=@app1"),
OsString::from("--depth=0"),
];
assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args);
}
#[test]
fn test_pnpm_subcommand_with_filter() {
let cli = Cli::try_parse_from([
"rtk", "pnpm", "--filter", "@app1", "--filter", "@app2", "list", "--filter", "@app3",
"--filter", "@app4", "--prod",
])
.unwrap();
match cli.command {
Commands::Pnpm {
filter,
command: PnpmCommands::List { depth, args },
} => {
assert_eq!(depth, 0);
assert_eq!(filter, vec!["@app1", "@app2"]);
assert_eq!(
args,
vec!["--filter", "@app3", "--filter", "@app4", "--prod"]
);
}
_ => panic!("Expected Pnpm List command"),
}
}
#[test]
fn test_git_push_u_flag_passes_through() {
let cli = Cli::try_parse_from(["rtk", "git", "push", "-u", "origin", "my-branch"]).unwrap();
assert!(
!cli.ultra_compact,
"-u on git push must NOT be consumed as --ultra-compact"
);
match cli.command {
Commands::Git {
command: GitCommands::Push { args },
..
} => {
assert!(
args.contains(&"-u".to_string()),
"-u must be forwarded to git push, got: {:?}",
args
);
}
_ => panic!("Expected Git Push command"),
}
}
#[test]
fn test_pnpm_subcommand_with_short_filter() {
let cli =
Cli::try_parse_from(["rtk", "pnpm", "-F", "@app1", "-F", "@app2", "list"]).unwrap();
match cli.command {
Commands::Pnpm { filter, .. } => {
assert_eq!(filter, vec!["@app1", "@app2"]);
}
_ => panic!("Expected Pnpm command"),
}
}
#[test]
fn test_pnpm_typecheck_without_filters() {
let cli = Cli::try_parse_from([
"rtk",
"pnpm",
"typecheck",
"--filter",
"@app3",
"--filter",
"@app4",
])
.unwrap();
match cli.command {
Commands::Pnpm { filter, command } => {
let warning = validate_pnpm_filters(&filter, &command);
assert!(filter.is_empty());
assert!(warning.is_none())
}
_ => panic!("Expected Pnpm Build command"),
}
}
#[test]
fn test_pnpm_typecheck_with_filters() {
let cli = Cli::try_parse_from([
"rtk",
"pnpm",
"--filter",
"@app1",
"--filter",
"@app2",
"typecheck",
"--filter",
"@app3",
"--filter",
"@app4",
])
.unwrap();
match cli.command {
Commands::Pnpm { filter, command } => {
let warning = validate_pnpm_filters(&filter, &command).unwrap();
assert_eq!(filter, vec!["@app1", "@app2"]);
assert_eq!(warning, "[rtk] warning: --filter is not yet supported for pnpm tsc, filters preceding the subcommand will be ignored")
}
_ => panic!("Expected Pnpm Build command"),
}
}
#[test]
fn test_ultra_compact_long_form_still_works() {
let cli = Cli::try_parse_from(["rtk", "--ultra-compact", "git", "status"]).unwrap();
assert!(
cli.ultra_compact,
"--ultra-compact long form must still enable ultra-compact mode"
);
}
#[test]
fn test_npx_unknown_tool_passthrough() {
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 Commands::Npx for unknown tool"),
}
}
}