Skip to content
Closed
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
46 changes: 46 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,52 @@ rtk pnpm list
rtk vitest run
```

## Optional: Shim Mode for Codex (Unix/macOS)

If you want Codex to run common tool names (`git`, `curl`, `docker`, `kubectl`, etc.) through RTK automatically, install Shim entrypoints:

```bash
# Install all Shim-eligible commands to ~/.local/rtk-shims/bin
rtk shim install

# Or install a focused subset while testing:
rtk shim install git gh curl
```

Notes:
- `rtk shim install` currently fails fast on Windows.
- Default shim location: `~/.local/rtk-shims/bin`

### PATH strategies for Codex

Use one of these depending on how aggressively you want to route commands:

```bash
# 1) One-shot (recommended for experiments):
# Only this Codex process gets shim-first PATH.
PATH="$HOME/.local/rtk-shims/bin:$PATH" codex

# 2) Current shell session:
# Useful when launching Codex multiple times in one terminal.
export PATH="$HOME/.local/rtk-shims/bin:$PATH"
codex

# 3) Persistent (every new shell):
# Add shim path in your shell rc, then reopen terminal.
echo 'export PATH="$HOME/.local/rtk-shims/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
codex
```

Quick sanity check before launching Codex:

```bash
command -v git
# Expected (shim-first): ~/.local/rtk-shims/bin/git
```

To disable shim-first behavior, remove the shim path from `PATH` (or start Codex without that PATH override).

## Uninstalling

### Complete Removal (Global Installations Only)
Expand Down
240 changes: 129 additions & 111 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod lint_cmd;
mod local_llm;
mod log_cmd;
mod ls;
mod metadata;
mod mypy_cmd;
mod next_cmd;
mod npm_cmd;
Expand All @@ -45,6 +46,7 @@ mod read;
mod rewrite_cmd;
mod ruff_cmd;
mod runner;
mod shim;
mod summary;
mod tee;
mod telemetry;
Expand Down Expand Up @@ -418,6 +420,12 @@ enum Commands {
create: bool,
},

/// Install operational_command shims
Shim {
#[command(subcommand)]
command: ShimCommands,
},

/// Vitest commands with compact output
Vitest {
#[command(subcommand)]
Expand Down Expand Up @@ -811,6 +819,27 @@ enum KubectlCommands {
Other(Vec<OsString>),
}

#[derive(Subcommand)]
enum ShimCommands {
/// Install operational_command shims that point to the rtk binary
Install {
/// Target directory for operational_command shims
#[arg(long)]
bin_dir: Option<PathBuf>,
/// Path to rtk binary (default: current executable)
#[arg(long)]
rtk_bin: Option<PathBuf>,
/// Replace existing entries
#[arg(long)]
force: bool,
/// Replace arbitrary existing files (not just existing shim symlinks)
#[arg(long)]
force_all: bool,
/// Explicit operational_commands to install (default: all Shim-eligible operational_commands)
operational_commands: Vec<String>,
},
}

#[derive(Subcommand)]
enum VitestCommands {
/// Run tests with filtered output (90% token reduction)
Expand Down Expand Up @@ -937,19 +966,18 @@ enum GoCommands {

/// RTK-only subcommands that should never fall back to raw execution.
/// If Clap fails to parse these, show the Clap error directly.
const RTK_META_COMMANDS: &[&str] = &[
"gain",
"discover",
"learn",
"init",
"config",
"proxy",
"hook-audit",
"cc-economics",
];

fn run_fallback(parse_error: clap::Error) -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
fn run_fallback(parse_error: clap::Error, parse_argv: &[OsString]) -> Result<()> {
// If argv[0] is a known-but-excluded operational_command (e.g. manually symlinked `gain -> rtk`),
// do not treat positional args as fallback executable names.
if shim::should_block_fallback_for_excluded_shim_command(parse_argv) {
parse_error.exit();
}

let args: Vec<String> = parse_argv
.iter()
.skip(1)
.map(|s| s.to_string_lossy().into_owned())
.collect();

// No args → show Clap's error (user ran just "rtk" with bad syntax)
if args.is_empty() {
Expand All @@ -958,7 +986,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<()> {

// RTK meta-commands should never fall back to raw execution.
// e.g. `rtk gain --badtypo` should show Clap's error, not try to run `gain` from $PATH.
if RTK_META_COMMANDS.contains(&args[0].as_str()) {
if metadata::is_rtk_meta_command(&args[0]) {
parse_error.exit();
}

Expand All @@ -968,7 +996,16 @@ 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 mut fallback_cmd = match shim::native_command(&args[0]) {
Ok(cmd) => cmd,
Err(e) => {
tracking::record_parse_failure_silent(&raw_command, &error_message, false);
eprintln!("[rtk: fallback failed: {}]", e);
parse_error.exit();
}
};

let status = fallback_cmd
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
Expand Down Expand Up @@ -1040,20 +1077,24 @@ fn main() -> Result<()> {
// Warn if installed hook is outdated (1/day, non-blocking)
hook_check::maybe_warn();

let cli = match Cli::try_parse() {
let Some(parse_argv) = shim::prepare_runtime_parse_argv()? else {
return Ok(());
};

let cli = match Cli::try_parse_from(parse_argv.clone()) {
Ok(cli) => cli,
Err(e) => {
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
e.exit();
}
return run_fallback(e);
return run_fallback(e, &parse_argv);
}
};

// Runtime integrity check for operational commands.
// Meta commands (init, gain, verify, config, etc.) skip the check
// because they don't go through the hook pipeline.
if is_operational_command(&cli.command) {
if metadata::is_operational_command_from_parse_argv(&parse_argv) {
integrity::runtime_check()?;
}

Expand Down Expand Up @@ -1490,6 +1531,31 @@ fn main() -> Result<()> {
}
}

Commands::Shim { command } => match command {
ShimCommands::Install {
bin_dir,
rtk_bin,
force,
force_all,
operational_commands,
} => {
let allowed = metadata::shim_eligible_top_level_commands();
let operational_commands =
shim::resolve_shim_operational_commands(&operational_commands, &allowed)?;
if operational_commands.is_empty() {
anyhow::bail!("no operational_commands to shim");
}

shim::install_operational_command_shims(
bin_dir,
rtk_bin,
force,
force_all,
&operational_commands,
)?;
}
},

Commands::Vitest { command } => match command {
VitestCommands::Run { args } => {
vitest_cmd::run(vitest_cmd::VitestCommand::Run, &args, cli.verbose)?;
Expand Down Expand Up @@ -1879,64 +1945,56 @@ fn main() -> Result<()> {
Ok(())
}

/// Returns true for commands that are invoked via the hook pipeline
/// (i.e., commands that process rewritten shell commands).
/// Meta commands (init, gain, verify, etc.) are excluded because
/// they are run directly by the user, not through the hook.
/// Returns true for commands that go through the hook pipeline
/// and therefore require integrity verification.
///
/// SECURITY: whitelist pattern — new commands are NOT integrity-checked
/// until explicitly added here. A forgotten command fails open (no check)
/// rather than creating false confidence about what's protected.
fn is_operational_command(cmd: &Commands) -> bool {
matches!(
cmd,
Commands::Ls { .. }
| Commands::Tree { .. }
| Commands::Read { .. }
| Commands::Smart { .. }
| Commands::Git { .. }
| Commands::Gh { .. }
| Commands::Pnpm { .. }
| Commands::Err { .. }
| Commands::Test { .. }
| Commands::Json { .. }
| Commands::Deps { .. }
| Commands::Env { .. }
| Commands::Find { .. }
| Commands::Diff { .. }
| Commands::Log { .. }
| 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::Pip { .. }
| Commands::Go { .. }
| Commands::GolangciLint { .. }
| Commands::Gt { .. }
)
}

#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;

#[test]
fn test_shim_install_parses() {
let cli = Cli::try_parse_from([
"rtk",
"shim",
"install",
"--force",
"--force-all",
"--bin-dir",
"/tmp/rtk-shims",
"--rtk-bin",
"/usr/local/bin/rtk",
"git",
"curl",
])
.unwrap();
match cli.command {
Commands::Shim { command } => match command {
ShimCommands::Install {
bin_dir,
rtk_bin,
force,
force_all,
operational_commands,
} => {
assert!(force);
assert!(force_all);
assert_eq!(bin_dir.as_deref(), Some(Path::new("/tmp/rtk-shims")));
assert_eq!(rtk_bin.as_deref(), Some(Path::new("/usr/local/bin/rtk")));
assert_eq!(operational_commands, vec!["git", "curl"]);
}
},
_ => panic!("Expected Shim command"),
}
}

#[test]
fn test_link_command_removed() {
let result = Cli::try_parse_from(["rtk", "link", "install", "git"]);
assert!(
result.is_err(),
"legacy 'link' command should not parse after shim rename"
);
}

#[test]
fn test_git_commit_single_message() {
let cli = Cli::try_parse_from(["rtk", "git", "commit", "-m", "fix: typo"]).unwrap();
Expand Down Expand Up @@ -2137,44 +2195,4 @@ mod tests {
}
}
}

#[test]
fn test_meta_commands_reject_bad_flags() {
// RTK meta-commands should produce parse errors (not fall through to raw execution).
// Skip "proxy" because it uses trailing_var_arg (accepts any args by design).
for cmd in RTK_META_COMMANDS {
if *cmd == "proxy" {
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_meta_command_list_is_complete() {
// Verify all meta-commands are in the guard list by checking they parse with valid syntax
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", "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
);
}
}
}
Loading