Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Bug Fixes

* **curl:** skip JSON schema replacement when schema is larger than original payload ([#297](https://github.com/rtk-ai/rtk/issues/297))
* **pnpm:** allow flag-first invocations like `rtk pnpm --filter <pkg> test` to pass through instead of failing Clap subcommand parsing ([#259](https://github.com/rtk-ai/rtk/issues/259))
* **toml-dsl:** fix regex overmatch on `tofu-plan/init/validate/fmt` and `mix-format/compile` — add `(\s|$)` word boundary to prevent matching subcommands (e.g. `tofu planet`, `mix formats`) ([#349](https://github.com/rtk-ai/rtk/issues/349))
* **toml-dsl:** remove 3 dead built-in filters (`docker-inspect`, `docker-compose-ps`, `pnpm-build`) — Clap routes these commands before `run_fallback`, so the TOML filters never fire ([#351](https://github.com/rtk-ai/rtk/issues/351))
* **toml-dsl:** `uv-sync` — remove `Resolved` short-circuit; it fires before the package list is printed, hiding installed packages ([#386](https://github.com/rtk-ai/rtk/issues/386))
Expand Down
239 changes: 168 additions & 71 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ enum Commands {

/// pnpm commands with ultra-compact output
Pnpm {
#[command(subcommand)]
command: PnpmCommands,
/// pnpm arguments (subcommand + options), supports flag-first forms like --filter
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<OsString>,
},

/// Run command and show only errors/warnings
Expand Down Expand Up @@ -786,48 +787,6 @@ enum GitCommands {
Other(Vec<OsString>),
}

#[derive(Subcommand)]
enum PnpmCommands {
/// List installed packages (ultra-dense)
List {
/// Depth level (default: 0)
#[arg(short, long, default_value = "0")]
depth: usize,
/// Additional pnpm arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Show outdated packages (condensed: "pkg: old → new")
Outdated {
/// Additional pnpm arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Install packages (filter progress bars)
Install {
/// Packages to install
packages: Vec<String>,
/// Additional pnpm arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Build (generic passthrough, no framework-specific filter)
Build {
/// Additional build arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Typecheck (delegates to tsc filter)
Typecheck {
/// Additional typecheck arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Passthrough: runs any unsupported pnpm subcommand directly
#[command(external_subcommand)]
Other(Vec<OsString>),
}

#[derive(Subcommand)]
enum DockerCommands {
/// List running containers
Expand Down Expand Up @@ -1241,6 +1200,130 @@ fn shell_split(input: &str) -> Vec<String> {
tokens
}

fn os_args_to_utf8(args: &[OsString]) -> Option<Vec<String>> {
args.iter()
.map(|arg| arg.to_str().map(|s| s.to_string()))
.collect()
}

fn extract_pnpm_list_depth(args: &[String]) -> (usize, Vec<String>) {
let mut depth = 0usize;
let mut passthrough_args = Vec::new();
let mut i = 0usize;

while i < args.len() {
match args[i].as_str() {
"--depth" => {
if let Some(value) = args.get(i + 1) {
if let Ok(parsed) = value.parse::<usize>() {
depth = parsed;
} else {
passthrough_args.push(args[i].clone());
passthrough_args.push(value.clone());
}
i += 2;
continue;
}
passthrough_args.push(args[i].clone());
}
arg if arg.starts_with("--depth=") => {
if let Some((_, value)) = arg.split_once('=') {
if let Ok(parsed) = value.parse::<usize>() {
depth = parsed;
} else {
passthrough_args.push(args[i].clone());
}
} else {
passthrough_args.push(args[i].clone());
}
}
"-d" => {
if let Some(value) = args.get(i + 1) {
if let Ok(parsed) = value.parse::<usize>() {
depth = parsed;
} else {
passthrough_args.push(args[i].clone());
passthrough_args.push(value.clone());
}
i += 2;
continue;
}
passthrough_args.push(args[i].clone());
}
_ => passthrough_args.push(args[i].clone()),
}
i += 1;
}

(depth, passthrough_args)
}

fn route_pnpm_command(args: &[OsString], verbose: u8) -> Result<()> {
if args.is_empty() {
return pnpm_cmd::run_passthrough(args, verbose);
}

let first = args
.first()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();

// Flag-first pnpm forms (e.g., `pnpm --filter pkg test`) must pass through untouched.
if first.starts_with('-') {
return pnpm_cmd::run_passthrough(args, verbose);
}

let utf8_args = match os_args_to_utf8(args) {
Some(v) => v,
None => return pnpm_cmd::run_passthrough(args, verbose),
};

let subcmd = utf8_args[0].as_str();
let rest = &utf8_args[1..];

match subcmd {
"list" | "ls" => {
let (depth, list_args) = extract_pnpm_list_depth(rest);
pnpm_cmd::run(pnpm_cmd::PnpmCommand::List { depth }, &list_args, verbose)?;
}
"outdated" => {
pnpm_cmd::run(pnpm_cmd::PnpmCommand::Outdated, rest, verbose)?;
}
"install" => {
let mut packages = Vec::new();
let mut install_args = Vec::new();
let mut seen_flag = false;

for arg in rest {
if seen_flag || arg.starts_with('-') {
seen_flag = true;
install_args.push(arg.clone());
} else {
packages.push(arg.clone());
}
}

pnpm_cmd::run(
pnpm_cmd::PnpmCommand::Install { packages },
&install_args,
verbose,
)?;
}
"build" => {
pnpm_cmd::run_passthrough(args, verbose)?;
}
"typecheck" => {
tsc_cmd::run(rest, verbose)?;
}
_ => {
pnpm_cmd::run_passthrough(args, verbose)?;
}
}

Ok(())
}

fn main() -> Result<()> {
// Fire-and-forget telemetry ping (1/day, non-blocking)
telemetry::maybe_ping();
Expand Down Expand Up @@ -1463,33 +1546,9 @@ fn main() -> Result<()> {
psql_cmd::run(&args, cli.verbose)?;
}

Commands::Pnpm { command } => match command {
PnpmCommands::List { depth, args } => {
pnpm_cmd::run(pnpm_cmd::PnpmCommand::List { depth }, &args, cli.verbose)?;
}
PnpmCommands::Outdated { args } => {
pnpm_cmd::run(pnpm_cmd::PnpmCommand::Outdated, &args, cli.verbose)?;
}
PnpmCommands::Install { packages, args } => {
pnpm_cmd::run(
pnpm_cmd::PnpmCommand::Install { packages },
&args,
cli.verbose,
)?;
}
PnpmCommands::Build { args } => {
let mut build_args: Vec<String> = vec!["build".into()];
build_args.extend(args);
let os_args: Vec<OsString> = build_args.into_iter().map(OsString::from).collect();
pnpm_cmd::run_passthrough(&os_args, cli.verbose)?;
}
PnpmCommands::Typecheck { args } => {
tsc_cmd::run(&args, cli.verbose)?;
}
PnpmCommands::Other(args) => {
pnpm_cmd::run_passthrough(&args, cli.verbose)?;
}
},
Commands::Pnpm { args } => {
route_pnpm_command(&args, cli.verbose)?;
}

Commands::Err { command } => {
let cmd = command.join(" ");
Expand Down Expand Up @@ -2579,4 +2638,42 @@ mod tests {
}
}
}

#[test]
fn test_pnpm_flag_first_args_parse_for_passthrough() {
let cli = Cli::try_parse_from([
"rtk",
"pnpm",
"--filter",
"mymodule",
"test",
"**/*.spec.ts",
])
.unwrap();

match cli.command {
Commands::Pnpm { args } => {
let args: Vec<String> = args
.iter()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert_eq!(args, vec!["--filter", "mymodule", "test", "**/*.spec.ts"]);
}
_ => panic!("Expected Pnpm command"),
}
}

#[test]
fn test_extract_pnpm_list_depth() {
let args = vec![
"--depth".to_string(),
"2".to_string(),
"--long".to_string(),
"-d".to_string(),
"1".to_string(),
];
let (depth, passthrough) = extract_pnpm_list_depth(&args);
assert_eq!(depth, 1);
assert_eq!(passthrough, vec!["--long"]);
}
}