use tokio::sync::OnceCell;
const RTK_SUPPORTED_COMMANDS: &[&str] = &[
"git",
"gh",
"glab",
"aws",
"psql",
"pnpm",
"npm",
"npx",
"cargo",
"docker",
"kubectl",
"grep",
"find",
"ls",
"tree",
"diff",
"curl",
"wget",
"jest",
"vitest",
"prisma",
"tsc",
"next",
"dotnet",
"playwright",
"prettier",
"eslint",
"ps",
"top",
"lsof",
"netstat",
"ss",
"journalctl",
"dmesg",
"dig",
"nslookup",
"host",
"traceroute",
];
const RTK_BLOCKLIST: &[&str] = &[
"rtk", "sudo", "ssh", "scp", "sftp", "rsync", "vim", "vi", "nvim", "nano", "emacs", "less", "more", "man", "python", "python3", "node", "mysql", "redis-cli", "psql", ];
#[derive(Debug, Clone)]
pub struct RtkResult {
pub rewritten_command: String,
pub was_rewritten: bool,
pub original_command: String,
}
static RTK_BINARY: OnceCell<Option<String>> = OnceCell::const_new();
async fn find_rtk_binary() -> Option<String> {
RTK_BINARY
.get_or_init(|| async {
if let Ok(exe_path) = std::env::current_exe()
&& let Some(exe_dir) = exe_path.parent()
{
let bundled_path = exe_dir.join("rtk");
if bundled_path.exists() && bundled_path.is_file() {
tracing::info!("RTK binary found bundled at: {:?}", bundled_path);
return Some(bundled_path.to_string_lossy().to_string());
}
let bin_path = exe_dir.join("bin").join("rtk");
if bin_path.exists() && bin_path.is_file() {
tracing::info!("RTK binary found bundled at: {:?}", bin_path);
return Some(bin_path.to_string_lossy().to_string());
}
}
match tokio::process::Command::new("which")
.arg("rtk")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.await
{
Ok(output) if output.status.success() => {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
tracing::info!("RTK binary found in PATH: {}", path);
Some("rtk".to_string())
}
Ok(_) => {
tracing::info!("RTK binary not found in PATH or bundled");
None
}
Err(_) => {
tracing::warn!("Failed to check for rtk binary availability");
None
}
}
})
.await
.clone()
}
pub async fn is_rtk_available() -> bool {
find_rtk_binary().await.is_some()
}
pub(crate) fn first_command_token(command: &str) -> &str {
for token in command.split_whitespace() {
if token.contains('=') && !token.starts_with('-') && !token.starts_with('/') {
continue;
}
return token;
}
""
}
pub(crate) fn is_rtk_supported(token: &str) -> bool {
let basename = token.rsplit('/').next().unwrap_or(token);
if RTK_BLOCKLIST.contains(&basename) {
return false;
}
RTK_SUPPORTED_COMMANDS.contains(&basename)
}
pub async fn rewrite_command(command: &str) -> Option<RtkResult> {
let rtk_binary = find_rtk_binary().await?;
let trimmed = command.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.starts_with("rtk ") || trimmed == "rtk" {
return None;
}
let first_token = first_command_token(trimmed);
if !is_rtk_supported(first_token) {
tracing::debug!(
"RTK: command '{}' not supported (token: '{}')",
command,
first_token
);
return None;
}
let rewritten = format!("{} {}", rtk_binary, trimmed);
tracing::debug!("RTK rewrote: '{}' -> '{}'", command, rewritten);
Some(RtkResult {
rewritten_command: rewritten,
was_rewritten: true,
original_command: command.to_string(),
})
}
#[allow(dead_code)]
pub async fn rewrite_command_string(command: &str) -> Option<String> {
rewrite_command(command).await.map(|r| r.rewritten_command)
}