From 363be7742ac59843fd8919798b9e20bb6c8ea65a Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 27 Jul 2026 11:48:27 +0800 Subject: [PATCH] feat(global-cli): pass the raw subcommand to the local CLI The local CLI runs a command under its canonical name, so it sees `fmt` whether the user wrote `vp fmt` or `vp format`, and has no way to recover the spelling from its own argv. The subcommand is now read from argv before `normalize_args` rewrites anything, and travels to the local CLI as `VP_RAW_SUBCOMMAND` alongside the canonical command name, which is unchanged. Nothing reads it yet. --- crates/vite_global_cli/src/cli.rs | 73 ++++++++++++++----- crates/vite_global_cli/src/commands/config.rs | 8 +- crates/vite_global_cli/src/commands/create.rs | 8 +- .../vite_global_cli/src/commands/delegate.rs | 11 ++- crates/vite_global_cli/src/commands/staged.rs | 8 +- crates/vite_global_cli/src/commands/vpr.rs | 3 +- crates/vite_global_cli/src/js_executor.rs | 16 +++- crates/vite_global_cli/src/main.rs | 18 +++-- crates/vite_shared/src/env_vars.rs | 7 ++ 9 files changed, 117 insertions(+), 35 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 163c2ded7a..6ba7ebdc98 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -844,9 +844,22 @@ fn prompt_reinstall_node_mismatches( .unwrap_or(false) } +/// The subcommand as the user wrote it, taken from `argv` before any rewriting. +/// +/// Parsing resolves a subcommand to one clap variant, which does not record the +/// spelling used, so it is read straight from the command line instead. +#[must_use] +pub fn raw_subcommand(argv: &[String]) -> Option<&str> { + argv.iter().skip(1).map(String::as_str).find(|arg| !arg.starts_with('-')) +} + /// Run the CLI command. -pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result { - run_command_with_options(cwd, args, RenderOptions::default()).await +pub async fn run_command( + cwd: AbsolutePathBuf, + args: Args, + raw_subcommand: Option<&str>, +) -> Result { + run_command_with_options(cwd, args, RenderOptions::default(), raw_subcommand).await } /// Run the CLI command with rendering options. @@ -854,6 +867,7 @@ pub async fn run_command_with_options( cwd: AbsolutePathBuf, args: Args, render_options: RenderOptions, + raw_subcommand: Option<&str>, ) -> Result { // Handle --version flag (Category B: delegates to JS) if args.version { @@ -887,13 +901,13 @@ pub async fn run_command_with_options( } // Category B: JS Script Commands - Commands::Create { args } => commands::create::execute(cwd, &args).await, + Commands::Create { args } => commands::create::execute(cwd, &args, raw_subcommand).await, Commands::Migrate { args } => commands::migrate::execute(cwd, &args).await, - Commands::Config { args } => commands::config::execute(cwd, &args).await, + Commands::Config { args } => commands::config::execute(cwd, &args, raw_subcommand).await, - Commands::Staged { args } => commands::staged::execute(cwd, &args).await, + Commands::Staged { args } => commands::staged::execute(cwd, &args, raw_subcommand).await, // Category C: Local CLI Delegation (stubs) Commands::Dev { args } => { @@ -901,7 +915,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "dev", &args).await + commands::delegate::execute(cwd, "dev", &args, raw_subcommand).await } Commands::Build { args } => { @@ -909,7 +923,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "build", &args).await + commands::delegate::execute(cwd, "build", &args, raw_subcommand).await } Commands::Test { args } => { @@ -917,7 +931,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "test", &args).await + commands::delegate::execute(cwd, "test", &args, raw_subcommand).await } Commands::Lint { args } => { @@ -926,9 +940,9 @@ pub async fn run_command_with_options( } maybe_print_runtime_header("lint", &args, render_options.show_header); if should_force_global_delegate("lint", &args) { - commands::delegate::execute_global(cwd, "lint", &args).await + commands::delegate::execute_global(cwd, "lint", &args, raw_subcommand).await } else { - commands::delegate::execute(cwd, "lint", &args).await + commands::delegate::execute(cwd, "lint", &args, raw_subcommand).await } } @@ -938,9 +952,9 @@ pub async fn run_command_with_options( } maybe_print_runtime_header("fmt", &args, render_options.show_header); if should_force_global_delegate("fmt", &args) { - commands::delegate::execute_global(cwd, "fmt", &args).await + commands::delegate::execute_global(cwd, "fmt", &args, raw_subcommand).await } else { - commands::delegate::execute(cwd, "fmt", &args).await + commands::delegate::execute(cwd, "fmt", &args, raw_subcommand).await } } @@ -949,7 +963,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "check", &args).await + commands::delegate::execute(cwd, "check", &args, raw_subcommand).await } Commands::Pack { args } => { @@ -957,7 +971,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "pack", &args).await + commands::delegate::execute(cwd, "pack", &args, raw_subcommand).await } Commands::Run { args } => { @@ -965,7 +979,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "run", &args).await + commands::delegate::execute(cwd, "run", &args, raw_subcommand).await } Commands::Exec { args } => { @@ -973,7 +987,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "exec", &args).await + commands::delegate::execute(cwd, "exec", &args, raw_subcommand).await } Commands::Preview { args } => { @@ -982,7 +996,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "preview", &args).await + commands::delegate::execute(cwd, "preview", &args, raw_subcommand).await } Commands::Cache { args } => { @@ -990,7 +1004,7 @@ pub async fn run_command_with_options( return Ok(ExitStatus::default()); } print_runtime_header(render_options.show_header); - commands::delegate::execute(cwd, "cache", &args).await + commands::delegate::execute(cwd, "cache", &args, raw_subcommand).await } Commands::Env(args) => commands::env::execute(cwd, args).await, @@ -1086,10 +1100,31 @@ pub fn try_parse_args_from_with_options( #[cfg(test)] mod tests { use super::{ - display_node_version, has_flag_before_terminator, is_same_node_version, + display_node_version, has_flag_before_terminator, is_same_node_version, raw_subcommand, should_force_global_delegate, should_suppress_header_for_subcommand, }; + fn argv(args: &[&str]) -> Vec { + args.iter().map(|arg| (*arg).to_string()).collect() + } + + #[test] + fn raw_subcommand_is_the_token_as_written() { + assert_eq!(raw_subcommand(&argv(&["vp", "fmt", "src/"])), Some("fmt")); + assert_eq!(raw_subcommand(&argv(&["vp", "format", "src/"])), Some("format")); + } + + #[test] + fn raw_subcommand_skips_leading_flags() { + assert_eq!(raw_subcommand(&argv(&["vp", "--silent", "install"])), Some("install")); + } + + #[test] + fn raw_subcommand_is_none_without_a_subcommand() { + assert_eq!(raw_subcommand(&argv(&["vp"])), None); + assert_eq!(raw_subcommand(&argv(&["vp", "--version"])), None); + } + #[test] fn detects_global_update_node_version_mismatch() { assert!(is_same_node_version("21.0.0", "v21.0.0")); diff --git a/crates/vite_global_cli/src/commands/config.rs b/crates/vite_global_cli/src/commands/config.rs index fb3983e0f1..1e0d1252a1 100644 --- a/crates/vite_global_cli/src/commands/config.rs +++ b/crates/vite_global_cli/src/commands/config.rs @@ -7,6 +7,10 @@ use vite_path::AbsolutePathBuf; use crate::error::Error; /// Execute the `config` command by delegating to local or global vite-plus. -pub async fn execute(cwd: AbsolutePathBuf, args: &[String]) -> Result { - super::delegate::execute(cwd, "config", args).await +pub async fn execute( + cwd: AbsolutePathBuf, + args: &[String], + raw_subcommand: Option<&str>, +) -> Result { + super::delegate::execute(cwd, "config", args, raw_subcommand).await } diff --git a/crates/vite_global_cli/src/commands/create.rs b/crates/vite_global_cli/src/commands/create.rs index 61925ce7b3..e8c9e8d6a3 100644 --- a/crates/vite_global_cli/src/commands/create.rs +++ b/crates/vite_global_cli/src/commands/create.rs @@ -7,8 +7,12 @@ use vite_path::AbsolutePathBuf; use crate::error::Error; /// Execute the `create` command by delegating to local or global vite-plus. -pub async fn execute(cwd: AbsolutePathBuf, args: &[String]) -> Result { - super::delegate::execute(cwd, "create", args).await +pub async fn execute( + cwd: AbsolutePathBuf, + args: &[String], + raw_subcommand: Option<&str>, +) -> Result { + super::delegate::execute(cwd, "create", args, raw_subcommand).await } #[cfg(test)] diff --git a/crates/vite_global_cli/src/commands/delegate.rs b/crates/vite_global_cli/src/commands/delegate.rs index 726169810e..6684a7e71d 100644 --- a/crates/vite_global_cli/src/commands/delegate.rs +++ b/crates/vite_global_cli/src/commands/delegate.rs @@ -7,12 +7,16 @@ use vite_path::AbsolutePathBuf; use crate::{error::Error, js_executor::JsExecutor}; /// Execute a command by delegating to the local `vite-plus` CLI. +/// +/// `raw_subcommand` is the subcommand as the user wrote it, which the local CLI +/// cannot recover from `command` alone once parsing has resolved it. pub async fn execute( cwd: AbsolutePathBuf, command: &str, args: &[String], + raw_subcommand: Option<&str>, ) -> Result { - let mut executor = JsExecutor::new(None); + let mut executor = JsExecutor::new(None).with_raw_subcommand(raw_subcommand); let mut full_args = vec![command.to_string()]; full_args.extend(args.iter().cloned()); executor.delegate_to_local_cli(&cwd, &full_args).await @@ -31,12 +35,15 @@ pub async fn execute_output( } /// Execute a command by delegating to the global `vite-plus` CLI. +/// +/// `raw_subcommand` is the subcommand as the user wrote it; see [`execute`]. pub async fn execute_global( cwd: AbsolutePathBuf, command: &str, args: &[String], + raw_subcommand: Option<&str>, ) -> Result { - let mut executor = JsExecutor::new(None); + let mut executor = JsExecutor::new(None).with_raw_subcommand(raw_subcommand); let mut full_args = vec![command.to_string()]; full_args.extend(args.iter().cloned()); executor.delegate_to_global_cli(&cwd, &full_args).await diff --git a/crates/vite_global_cli/src/commands/staged.rs b/crates/vite_global_cli/src/commands/staged.rs index fa78bd1a24..0bbbcd17ff 100644 --- a/crates/vite_global_cli/src/commands/staged.rs +++ b/crates/vite_global_cli/src/commands/staged.rs @@ -7,6 +7,10 @@ use vite_path::AbsolutePathBuf; use crate::error::Error; /// Execute the `staged` command by delegating to local or global vite-plus. -pub async fn execute(cwd: AbsolutePathBuf, args: &[String]) -> Result { - super::delegate::execute(cwd, "staged", args).await +pub async fn execute( + cwd: AbsolutePathBuf, + args: &[String], + raw_subcommand: Option<&str>, +) -> Result { + super::delegate::execute(cwd, "staged", args, raw_subcommand).await } diff --git a/crates/vite_global_cli/src/commands/vpr.rs b/crates/vite_global_cli/src/commands/vpr.rs index e043b9c8b0..e304ce12ae 100644 --- a/crates/vite_global_cli/src/commands/vpr.rs +++ b/crates/vite_global_cli/src/commands/vpr.rs @@ -15,7 +15,8 @@ pub async fn execute_vpr(args: &[String], cwd: &AbsolutePath) -> i32 { } let cwd_buf = cwd.to_absolute_path_buf(); - match super::delegate::execute(cwd_buf, "run", args).await { + // `vpr` is a shim, not a subcommand, so no subcommand was written. + match super::delegate::execute(cwd_buf, "run", args, None).await { Ok(status) => status.code().unwrap_or(1), Err(e) => { output::error(&e.to_string()); diff --git a/crates/vite_global_cli/src/js_executor.rs b/crates/vite_global_cli/src/js_executor.rs index 3c2c6b92ae..dd85b07c8c 100644 --- a/crates/vite_global_cli/src/js_executor.rs +++ b/crates/vite_global_cli/src/js_executor.rs @@ -31,6 +31,8 @@ pub struct JsExecutor { project_runtime: Option, /// Directory containing JS scripts (from `VITE_GLOBAL_CLI_JS_SCRIPTS_DIR`) scripts_dir: Option, + /// Subcommand as the user wrote it, forwarded to the CLI this one runs + raw_subcommand: Option, } impl JsExecutor { @@ -41,7 +43,16 @@ impl JsExecutor { /// If not provided, will be auto-detected from the binary location. #[must_use] pub const fn new(scripts_dir: Option) -> Self { - Self { cli_runtime: None, project_runtime: None, scripts_dir } + Self { cli_runtime: None, project_runtime: None, scripts_dir, raw_subcommand: None } + } + + /// Forward the subcommand as the user wrote it to the CLI this one runs. + /// + /// A command runs under its canonical name, so the spelling the user used is + /// otherwise lost on the way down. + pub fn with_raw_subcommand(mut self, raw_subcommand: Option<&str>) -> Self { + self.raw_subcommand = raw_subcommand.map(ToOwned::to_owned); + self } /// Get the JS scripts directory. @@ -340,6 +351,9 @@ impl JsExecutor { let mut cmd = Self::create_js_command(node_binary, bin_prefix); cmd.arg(entry_point.as_path()).args(args).current_dir(project_path.as_path()); + if let Some(raw_subcommand) = &self.raw_subcommand { + cmd.env(vite_shared::env_vars::VP_RAW_SUBCOMMAND, raw_subcommand); + } Ok(cmd) } diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 5a90eef107..aad56e9738 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -35,7 +35,7 @@ use vite_shared::output; pub use crate::cli::try_parse_args_from; use crate::cli::{ - RenderOptions, command_with_help, run_command, run_command_with_options, + RenderOptions, command_with_help, raw_subcommand, run_command, run_command_with_options, try_parse_args_from_with_options, }; @@ -224,9 +224,11 @@ fn clap_error_to_exit_code(e: &clap::Error) -> ExitCode { async fn run_corrected_args(cwd: &vite_path::AbsolutePathBuf, raw_args: &[String]) -> ExitCode { let render_options = RenderOptions { show_header: false }; - let args_with_program = std::iter::once("vp".to_string()).chain(raw_args.iter().cloned()); - let normalized_args = normalize_args(args_with_program.collect()); - + let args_with_program: Vec = + std::iter::once("vp".to_string()).chain(raw_args.iter().cloned()).collect(); + // The subcommand as written, taken before `normalize_args` can rewrite it. + let raw_subcommand = raw_subcommand(&args_with_program).map(str::to_owned); + let normalized_args = normalize_args(args_with_program); let parsed = match try_parse_args_from_with_options(normalized_args, render_options) { Ok(args) => args, Err(e) => { @@ -235,7 +237,9 @@ async fn run_corrected_args(cwd: &vite_path::AbsolutePathBuf, raw_args: &[String } }; - match run_command_with_options(cwd.clone(), parsed, render_options).await { + match run_command_with_options(cwd.clone(), parsed, render_options, raw_subcommand.as_deref()) + .await + { Ok(exit_status) => exit_status_to_exit_code(exit_status), Err(e) => { if e.is_user_message() { @@ -353,6 +357,8 @@ async fn main() -> ExitCode { // Capture user args (excluding argv0) before normalization. let raw_args = args[1..].to_vec(); + // The subcommand as written, taken before `normalize_args` can rewrite it. + let raw_subcommand = raw_subcommand(&args).map(str::to_owned); // Normalize arguments (list/ls aliases, help rewriting) let normalized_args = normalize_args(args); @@ -427,7 +433,7 @@ async fn main() -> ExitCode { clap_error_to_exit_code(&e) } } - Ok(args) => match run_command(cwd.clone(), args).await { + Ok(args) => match run_command(cwd.clone(), args, raw_subcommand.as_deref()).await { Ok(exit_status) => exit_status_to_exit_code(exit_status), Err(e) => { if e.is_user_message() { diff --git a/crates/vite_shared/src/env_vars.rs b/crates/vite_shared/src/env_vars.rs index 7e3de5b0dd..7f61af626c 100644 --- a/crates/vite_shared/src/env_vars.rs +++ b/crates/vite_shared/src/env_vars.rs @@ -74,6 +74,13 @@ pub const VP_SHIM_TOOL: &str = "VP_SHIM_TOOL"; /// before forwarding to the actual tool. pub const VP_SHIM_WRAPPER: &str = "VP_SHIM_WRAPPER"; +/// The subcommand as the user wrote it, passed from the global CLI to the local +/// one. +/// +/// A command runs under its canonical name (`vp format` runs `fmt`), which loses +/// the spelling. This carries the original alongside it. +pub const VP_RAW_SUBCOMMAND: &str = "VP_RAW_SUBCOMMAND"; + /// Path to the vp binary, passed to JS scripts so they can invoke CLI commands. pub const VP_CLI_BIN: &str = "VP_CLI_BIN";