diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c203a381..c562b9d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features * rewrite engine, OpenCode support, hook system improvements ([#539](https://github.com/rtk-ai/rtk/issues/539)) ([c1de10d](https://github.com/rtk-ai/rtk/commit/c1de10d94c0a35f825b71713e2db4624310c03d1)) +* **bun:** add Bun runtime support — install/add/remove, test, build, run, pm ls (~80-90% reduction) +* **bunx:** add Bunx tool execution with smart routing (tsc→tsc filter, eslint→lint filter) +* **deno:** add Deno runtime support — test, lint, check, run, task, compile, install (~90% reduction) ## [0.28.2](https://github.com/rtk-ai/rtk/compare/v0.28.1...v0.28.2) (2026-03-10) diff --git a/README.md b/README.md index e9663830d..d776d1cc7 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,17 @@ rtk bundle install # Ruby gems (strip Using lines) rtk prisma generate # Schema generation (no ASCII art) ``` +### Runtimes +```bash +rtk bun install # Strip progress bars (~80% reduction) +rtk bun test # Failures only (-90%) +rtk bun build # Errors only +rtk bunx tsc # Smart routing to tsc filter +rtk deno test # Failures only (-90%) +rtk deno lint # Strip download lines + tee recovery +rtk deno check # Type check errors only +``` + ### AWS ```bash rtk aws sts get-caller-identity # One-line identity diff --git a/src/cmds/js/bun_cmd.rs b/src/cmds/js/bun_cmd.rs new file mode 100644 index 000000000..bb0462384 --- /dev/null +++ b/src/cmds/js/bun_cmd.rs @@ -0,0 +1,350 @@ +//! Filters bun output — install logs, package lists, and pm commands. + +use crate::core::tracking; +use crate::core::utils::{exit_code_from_output, resolved_command, strip_ansi, truncate}; +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::collections::HashMap; +use std::ffi::OsString; + +/// JSON structure for `bun pm ls --json` output. +#[derive(Debug, Deserialize)] +struct BunPmPackage { + version: Option, +} + +/// Validate package names to prevent command injection (parity with pnpm_cmd). +fn is_valid_package_name(name: &str) -> bool { + if name.is_empty() || name.len() > 214 { + return false; + } + if name.contains("..") { + return false; + } + name.chars() + .all(|c| c.is_alphanumeric() || matches!(c, '@' | '/' | '-' | '_' | '.')) +} + +/// Filter bun install/add/remove output — strip progress lines, version headers, empty lines. +pub fn filter_bun_install(output: &str) -> String { + let cleaned = strip_ansi(output); + let mut result = Vec::new(); + + for line in cleaned.lines() { + let trimmed = line.trim(); + + if trimmed.is_empty() { + continue; + } + + // Skip progress lines like "[1/5] ..." + if trimmed.starts_with('[') { + if let Some(close) = trimmed.find(']') { + let after_bracket = trimmed[close + 1..].trim(); + if after_bracket.ends_with("...") { + let bracket_content = &trimmed[1..close]; + if bracket_content.contains('/') { + let parts: Vec<&str> = bracket_content.split('/').collect(); + if parts.len() == 2 + && parts[0].trim().parse::().is_ok() + && parts[1].trim().parse::().is_ok() + { + continue; + } + } + } + } + } + + // Skip version headers like "bun install v1.1.0" / "bun add v1.1.0" / "bun remove v1.1.0" + if (trimmed.starts_with("bun install v") + || trimmed.starts_with("bun add v") + || trimmed.starts_with("bun remove v")) + && trimmed.split_whitespace().count() <= 4 + { + continue; + } + + result.push(trimmed); + } + + if result.is_empty() { + "ok".to_string() + } else { + result.join("\n") + } +} + +/// Parse JSON output from `bun pm ls --json`. +pub fn filter_bun_pm_ls_json(raw: &str) -> Option { + let packages: HashMap = serde_json::from_str(raw).ok()?; + + if packages.is_empty() { + return None; + } + + let mut entries: Vec = packages + .iter() + .map(|(name, pkg)| { + if let Some(ver) = &pkg.version { + format!("{}@{}", name, ver) + } else { + name.clone() + } + }) + .collect(); + + entries.sort(); + + let count = entries.len(); + let mut result = format!("{} deps\n", count); + result.push_str(&entries.join("\n")); + + Some(result) +} + +/// Text fallback for `bun pm ls`. +pub fn filter_bun_pm_ls_text(raw: &str) -> String { + let lines: Vec<&str> = raw.lines().filter(|l| !l.trim().is_empty()).collect(); + + if lines.is_empty() { + return "ok".to_string(); + } + + let joined = lines.join("\n"); + truncate(&joined, 500) +} + +/// Run `bun install`, `bun add`, or `bun remove` with filtered output. +pub fn run_pkg(subcmd: &str, args: &[String], verbose: u8) -> Result { + let timer = tracking::TimedExecution::start(); + + // Validate package names in args (skip flags starting with -) + for arg in args { + if !arg.starts_with('-') && !is_valid_package_name(arg) { + anyhow::bail!("Invalid package name: {}", arg); + } + } + + let mut cmd = resolved_command("bun"); + cmd.arg(subcmd); + for arg in args { + cmd.arg(arg); + } + + if verbose > 0 { + eprintln!("Running: bun {} {}", subcmd, args.join(" ")); + } + + let output = cmd + .output() + .with_context(|| format!("Failed to run bun {}. Is bun installed?", subcmd))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !output.status.success() { + eprint!("{}", stderr); + } + + let filtered = filter_bun_install(&stdout); + println!("{}", filtered); + + let combined = format!("{}{}", stdout, stderr); + timer.track( + &format!("bun {} {}", subcmd, args.join(" ")), + &format!("rtk bun {} {}", subcmd, args.join(" ")), + &combined, + &filtered, + ); + + Ok(exit_code_from_output(&output, "bun")) +} + +pub fn run_pm_ls(args: &[String], verbose: u8) -> Result { + let timer = tracking::TimedExecution::start(); + + let mut cmd = resolved_command("bun"); + cmd.arg("pm").arg("ls"); + if !args.iter().any(|a| a == "--json") { + cmd.arg("--json"); + } + for arg in args { + cmd.arg(arg); + } + + if verbose > 0 { + eprintln!("Running: bun pm ls --json {}", args.join(" ")); + } + + let output = cmd + .output() + .context("Failed to run bun pm ls. Is bun installed?")?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !output.status.success() { + eprint!("{}", stderr); + } + + let filtered = if let Some(json_result) = filter_bun_pm_ls_json(&stdout) { + json_result + } else { + filter_bun_pm_ls_text(&stdout) + }; + + println!("{}", filtered); + + let combined = format!("{}{}", stdout, stderr); + timer.track( + &format!("bun pm ls {}", args.join(" ")), + &format!("rtk bun pm ls {}", args.join(" ")), + &combined, + &filtered, + ); + + Ok(exit_code_from_output(&output, "bun")) +} + +/// Passthrough for `bun run` and other unfiltered subcommands. +pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result { + crate::core::runner::run_passthrough("bun", args, verbose) +} + +#[cfg(test)] +mod tests { + use super::*; + fn count_tokens(text: &str) -> usize { + text.split_whitespace().count() + } + + #[test] + fn test_filter_bun_install_strips_progress() { + let output = r#"bun install v1.1.0 +[1/5] Resolving packages... +[2/5] Fetching packages... +[3/5] Linking packages... +[4/5] Building fresh packages... +[5/5] Cleaning up... + ++ installed express@4.18.2 ++ installed lodash@4.17.21 +3 packages installed in 1.2s +"#; + let result = filter_bun_install(output); + assert!(!result.contains("[1/5]")); + assert!(!result.contains("bun install v1.1.0")); + assert!(result.contains("express")); + assert!(result.contains("3 packages installed")); + } + + #[test] + fn test_filter_bun_install_token_savings() { + // Realistic bun install output with many progress lines and version header + let input = r#"bun install v1.2.5 (a1b2c3d4) +[1/10] Resolving packages... +[2/10] Fetching packages... +[3/10] Linking dependencies... +[4/10] Building fresh packages... +[5/10] Compiling native modules... +[6/10] Running lifecycle scripts... +[7/10] Generating lockfile... +[8/10] Deduplicating packages... +[9/10] Cleaning cache... +[10/10] Writing lockfile... + ++ installed express@4.18.2 ++ installed lodash@4.17.21 +10 packages installed in 2.3s +"#; + let output = filter_bun_install(input); + let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0); + assert!( + savings >= 60.0, + "Bun install filter: expected >=60% savings, got {:.1}%", + savings + ); + } + + #[test] + fn test_filter_bun_install_empty_output() { + let output = "\n\n\n"; + let result = filter_bun_install(output); + assert_eq!(result, "ok"); + } + + #[test] + fn test_filter_bun_install_strips_ansi() { + let output = "\x1b[32m[1/3] Resolving packages...\x1b[0m\n\x1b[32m[2/3] Fetching packages...\x1b[0m\n\x1b[32m[3/3] Linking packages...\x1b[0m\n+ installed express@4.18.2\n"; + let result = filter_bun_install(output); + assert!(!result.contains("[1/3]")); + assert!(result.contains("express")); + } + + #[test] + fn test_filter_bun_install_preserves_errors() { + let output = r#"bun install v1.1.0 +[1/4] Resolving packages... +error: PackageNotFound - "nonexistent-pkg" not found in registry +"#; + let result = filter_bun_install(output); + assert!(result.contains("error:")); + assert!(result.contains("nonexistent-pkg")); + } + + #[test] + fn test_filter_bun_install_handles_remove() { + let output = "bun remove v1.1.0\n- removed express@4.18.2\n1 package removed in 0.5s\n"; + let result = filter_bun_install(output); + assert!(!result.contains("bun remove v1.1.0")); + assert!(result.contains("removed express")); + } + + #[test] + fn test_filter_bun_pm_ls_json() { + let json = r#"{ + "express": {"version": "4.18.2"}, + "lodash": {"version": "4.17.21"}, + "axios": {"version": "1.6.0"} + }"#; + let result = filter_bun_pm_ls_json(json).expect("should parse"); + assert!(result.starts_with("3 deps")); + let lines: Vec<&str> = result.lines().collect(); + assert_eq!(lines[1], "axios@1.6.0"); + assert_eq!(lines[2], "express@4.18.2"); + assert_eq!(lines[3], "lodash@4.17.21"); + } + + #[test] + fn test_filter_bun_pm_ls_json_empty() { + let result = filter_bun_pm_ls_json("{}"); + assert!(result.is_none()); + } + + #[test] + fn test_filter_bun_pm_ls_json_invalid() { + let result = filter_bun_pm_ls_json("not json"); + assert!(result.is_none()); + } + + #[test] + fn test_filter_bun_pm_ls_text_truncates() { + let long_output = (0..100) + .map(|i| format!("pkg-{i}@1.0.0")) + .collect::>() + .join("\n"); + let result = filter_bun_pm_ls_text(&long_output); + assert!(result.len() <= 520); + } + + #[test] + fn test_is_valid_package_name() { + assert!(is_valid_package_name("express")); + assert!(is_valid_package_name("@types/node")); + assert!(is_valid_package_name("lodash.merge")); + assert!(!is_valid_package_name("")); + assert!(!is_valid_package_name("../etc/passwd")); + assert!(!is_valid_package_name("pkg;rm -rf /")); + } +} diff --git a/src/cmds/js/deno_cmd.rs b/src/cmds/js/deno_cmd.rs new file mode 100644 index 000000000..aa81d21bb --- /dev/null +++ b/src/cmds/js/deno_cmd.rs @@ -0,0 +1,168 @@ +//! Filters deno output — lint, check, and task command output. + +use crate::core::tracking; +use crate::core::utils::{exit_code_from_output, resolved_command, strip_ansi}; +use anyhow::{Context, Result}; +use std::ffi::OsString; + +/// Filter deno output: strip ANSI codes, download lines, and empty lines. +pub fn filter_deno_output(output: &str) -> String { + let cleaned = strip_ansi(output); + let filtered: Vec<&str> = cleaned + .lines() + .filter(|line| { + let trimmed = line.trim(); + !trimmed.is_empty() && !trimmed.starts_with("Download ") + }) + .collect(); + + if filtered.is_empty() { + "ok".to_string() + } else { + filtered.join("\n") + } +} + +/// Run a deno subcommand with filtered output and tee recovery. +fn run_filtered_subcmd(subcmd: &str, args: &[String], verbose: u8) -> Result { + let timer = tracking::TimedExecution::start(); + + let mut cmd = resolved_command("deno"); + cmd.arg(subcmd); + for arg in args { + cmd.arg(arg); + } + + if verbose > 0 { + eprintln!("Running: deno {} {}", subcmd, args.join(" ")); + } + + let output = cmd + .output() + .with_context(|| format!("Failed to run deno {}. Is Deno installed?", subcmd))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let combined = format!("{}{}", stdout, stderr); + + let exit_code = exit_code_from_output(&output, "deno"); + let filtered = filter_deno_output(&combined); + + if let Some(hint) = crate::core::tee::tee_and_hint(&combined, &format!("deno_{}", subcmd), exit_code) { + println!("{}\n{}", filtered, hint); + } else { + println!("{}", filtered); + } + + timer.track( + &format!("deno {} {}", subcmd, args.join(" ")), + &format!("rtk deno {} {}", subcmd, args.join(" ")), + &combined, + &filtered, + ); + + Ok(exit_code) +} + +pub fn run_lint(args: &[String], verbose: u8) -> Result { + run_filtered_subcmd("lint", args, verbose) +} + +pub fn run_check(args: &[String], verbose: u8) -> Result { + run_filtered_subcmd("check", args, verbose) +} + +/// Passthrough for `deno run`, `deno task`, and other unfiltered subcommands. +pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result { + crate::core::runner::run_passthrough("deno", args, verbose) +} + +#[cfg(test)] +mod tests { + use super::*; + fn count_tokens(text: &str) -> usize { + text.split_whitespace().count() + } + + #[test] + fn test_filter_deno_output_strips_download() { + let input = r#"Download https://deno.land/std@0.200.0/path/mod.ts +Download https://deno.land/x/oak@v12.6.1/mod.ts +error: Expected ';' at main.ts:5:10 +some warning here"#; + + let result = filter_deno_output(input); + assert!(!result.contains("Download ")); + assert!(result.contains("error: Expected ';' at main.ts:5:10")); + assert!(result.contains("some warning here")); + } + + #[test] + fn test_filter_deno_output_token_savings() { + // Realistic deno output with many download lines before actual content + let input = r#"Download https://deno.land/std@0.200.0/path/mod.ts +Download https://deno.land/x/oak@v12.6.1/mod.ts +Download https://deno.land/std@0.200.0/fmt/colors.ts +Download https://deno.land/std@0.200.0/io/mod.ts +Download https://deno.land/std@0.200.0/http/server.ts +Download https://deno.land/std@0.200.0/async/mod.ts +Download https://deno.land/std@0.200.0/testing/asserts.ts +Download https://deno.land/std@0.200.0/encoding/base64.ts +Download https://deno.land/std@0.200.0/crypto/mod.ts +Download https://deno.land/std@0.200.0/streams/mod.ts +Download https://deno.land/std@0.200.0/bytes/mod.ts +Download https://deno.land/std@0.200.0/collections/mod.ts +Download https://deno.land/std@0.200.0/datetime/mod.ts +Download https://deno.land/std@0.200.0/flags/mod.ts +Download https://deno.land/std@0.200.0/uuid/mod.ts +Check file:///project/main.ts +error: Expected ';' at main.ts:5:10 +warning: Unused variable 'x' at main.ts:3:7 +"#; + let output = filter_deno_output(input); + let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0); + assert!( + savings >= 60.0, + "Deno filter: expected >=60% savings, got {:.1}%", + savings + ); + } + + #[test] + fn test_filter_deno_output_empty() { + let input = r#"Download https://deno.land/std@0.200.0/path/mod.ts + +Download https://deno.land/x/oak@v12.6.1/mod.ts + +"#; + + let result = filter_deno_output(input); + assert_eq!(result, "ok"); + } + + #[test] + fn test_filter_deno_strips_ansi() { + let input = "\x1b[33mDownload https://deno.land/std@0.200.0/path/mod.ts\x1b[0m\n\x1b[31merror: something\x1b[0m\n"; + let result = filter_deno_output(input); + assert!(!result.contains("Download")); + assert!(result.contains("error: something")); + } + + #[test] + fn test_filter_deno_preserves_check_lines() { + let input = "Check file:///project/main.ts\n"; + let result = filter_deno_output(input); + assert!(result.contains("Check")); + } + + #[test] + fn test_filter_deno_preserves_errors_strips_downloads() { + let input = r#"Download https://deno.land/std@0.210.0/path/mod.ts +error: Module not found "https://deno.land/x/nonexistent/mod.ts" +"#; + let result = filter_deno_output(input); + assert!(result.contains("error:")); + assert!(result.contains("Module not found")); + assert!(!result.contains("Download")); + } +} diff --git a/src/discover/rules.rs b/src/discover/rules.rs index b315edd77..00a3b019b 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -359,6 +359,38 @@ pub const RULES: &[RtkRule] = &[ subcmd_savings: &[], subcmd_status: &[], }, + // Bun/Deno + RtkRule { + pattern: r"^bun\s+(install|add|remove|test|build|run|pm)", + rtk_cmd: "rtk bun", + rewrite_prefixes: &["bun"], + category: "PackageManager", + savings_pct: 75.0, + subcmd_savings: &[("test", 90.0), ("install", 80.0)], + subcmd_status: &[("run", RtkStatus::Passthrough)], + }, + RtkRule { + pattern: r"^bunx\s+", + rtk_cmd: "rtk bunx", + rewrite_prefixes: &["bunx"], + category: "PackageManager", + savings_pct: 70.0, + subcmd_savings: &[], + subcmd_status: &[], + }, + RtkRule { + pattern: r"^deno\s+(test|lint|check|run|task|compile|install)", + rtk_cmd: "rtk deno", + rewrite_prefixes: &["deno"], + category: "Build", + savings_pct: 75.0, + subcmd_savings: &[("test", 90.0), ("lint", 80.0)], + subcmd_status: &[ + ("run", RtkStatus::Passthrough), + ("task", RtkStatus::Passthrough), + ], + }, + // TOML-filtered commands RtkRule { pattern: r"^ansible-playbook\b", rtk_cmd: "rtk ansible-playbook", diff --git a/src/main.rs b/src/main.rs index 77a4be8c3..96327ca7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,8 @@ use cmds::dotnet::{binlog, dotnet_cmd, dotnet_format_report, dotnet_trx}; use cmds::git::{diff_cmd, gh_cmd, git, 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, + bun_cmd, deno_cmd, lint_cmd, next_cmd, npm_cmd, playwright_cmd, pnpm_cmd, prettier_cmd, + prisma_cmd, tsc_cmd, vitest_cmd, }; use cmds::python::{mypy_cmd, pip_cmd, pytest_cmd, ruff_cmd}; use cmds::ruby::{rake_cmd, rspec_cmd, rubocop_cmd}; @@ -505,6 +505,19 @@ enum Commands { args: Vec, }, + /// Bun runtime commands with compact output + Bun { + #[command(subcommand)] + command: BunCommands, + }, + + /// bunx with passthrough + auto-filter + Bunx { + /// bunx arguments + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Curl with auto-JSON detection and schema output Curl { /// Curl arguments (URL + options) @@ -635,6 +648,12 @@ enum Commands { args: Vec, }, + /// Deno runtime commands with compact output + Deno { + #[command(subcommand)] + command: DenoCommands, + }, + /// Go commands with compact output Go { #[command(subcommand)] @@ -1032,6 +1051,90 @@ enum GoCommands { Other(Vec), } +#[derive(Subcommand)] +enum BunCommands { + /// Install packages (filter progress bars) + Install { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Run scripts + Run { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Build project (errors only) + Build { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Test with compact output (failures only) + Test { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Add packages + Add { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Remove packages + Remove { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Package manager commands (pm ls, etc.) + Pm { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Passthrough: runs any unsupported bun subcommand directly + #[command(external_subcommand)] + Other(Vec), +} + +#[derive(Subcommand)] +enum DenoCommands { + /// Run a script + Run { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Type-check without running + Check { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Lint source files + Lint { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Run tests (failures only) + Test { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Run a task from deno.json + Task { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Compile to standalone executable (errors only) + Compile { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Install dependencies + Install { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Passthrough + #[command(external_subcommand)] + Other(Vec), +} + /// 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] = &[ @@ -1776,6 +1879,83 @@ fn run_cli() -> Result { CargoCommands::Other(args) => cargo_cmd::run_passthrough(&args, cli.verbose)?, }, + Commands::Bun { command } => match command { + BunCommands::Install { args } => bun_cmd::run_pkg("install", &args, cli.verbose)?, + BunCommands::Add { args } => bun_cmd::run_pkg("add", &args, cli.verbose)?, + BunCommands::Remove { args } => bun_cmd::run_pkg("remove", &args, cli.verbose)?, + BunCommands::Run { args } => { + let os_args: Vec = std::iter::once(OsString::from("run")) + .chain(args.into_iter().map(OsString::from)) + .collect(); + bun_cmd::run_passthrough(&os_args, cli.verbose)? + } + BunCommands::Build { args } => { + let cmd = format!("bun build {}", args.join(" ")); + runner::run_err(&cmd, cli.verbose)? + } + BunCommands::Test { args } => { + let cmd = format!("bun test {}", args.join(" ")); + runner::run_test(&cmd, cli.verbose)? + } + BunCommands::Pm { args } => { + if args.first().map(|s| s.as_str()) == Some("ls") { + bun_cmd::run_pm_ls(&args[1..], cli.verbose)? + } else { + let os_args: Vec = std::iter::once(OsString::from("pm")) + .chain(args.into_iter().map(OsString::from)) + .collect(); + bun_cmd::run_passthrough(&os_args, cli.verbose)? + } + } + BunCommands::Other(args) => bun_cmd::run_passthrough(&args, cli.verbose)?, + }, + + Commands::Bunx { args } => { + if args.is_empty() { + anyhow::bail!("bunx 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)?, + _ => { + let cmd = format!("bunx {}", args.join(" ")); + runner::run_err(&cmd, cli.verbose)? + } + } + } + + Commands::Deno { command } => match command { + DenoCommands::Test { args } => { + let cmd = format!("deno test {}", args.join(" ")); + runner::run_test(&cmd, cli.verbose)? + } + DenoCommands::Check { args } => deno_cmd::run_check(&args, cli.verbose)?, + DenoCommands::Lint { args } => deno_cmd::run_lint(&args, cli.verbose)?, + DenoCommands::Run { args } => { + let os_args: Vec = std::iter::once(OsString::from("run")) + .chain(args.into_iter().map(OsString::from)) + .collect(); + deno_cmd::run_passthrough(&os_args, cli.verbose)? + } + DenoCommands::Task { args } => { + let os_args: Vec = std::iter::once(OsString::from("task")) + .chain(args.into_iter().map(OsString::from)) + .collect(); + deno_cmd::run_passthrough(&os_args, cli.verbose)? + } + DenoCommands::Compile { args } => { + let cmd = format!("deno compile {}", args.join(" ")); + runner::run_err(&cmd, cli.verbose)? + } + DenoCommands::Install { args } => { + let os_args: Vec = std::iter::once(OsString::from("install")) + .chain(args.into_iter().map(OsString::from)) + .collect(); + deno_cmd::run_passthrough(&os_args, cli.verbose)? + } + DenoCommands::Other(args) => deno_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)?, @@ -2130,6 +2310,9 @@ fn is_operational_command(cmd: &Commands) -> bool { | Commands::Go { .. } | Commands::GolangciLint { .. } | Commands::Gt { .. } + | Commands::Bun { .. } + | Commands::Bunx { .. } + | Commands::Deno { .. } ) }