Skip to content
Merged
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
59 changes: 57 additions & 2 deletions crates/vite_global_cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,16 @@ fn skip_clap_unified_help(command: &str) -> bool {
)
}

fn should_skip_parent_help_for_unknown_direct_nested_child(
command_path: &[String],
argv: &[String],
index: usize,
) -> bool {
matches!(command_path, [command] if matches!(command.as_str(), "pm" | "env"))
&& argv.get(index).is_some_and(|arg| !arg.starts_with('-'))
&& has_help_flag_before_terminator(&argv[index..])
}

pub fn maybe_print_unified_clap_subcommand_help(argv: &[String]) -> bool {
if argv.len() < 3 {
return false;
Expand All @@ -977,6 +987,10 @@ pub fn maybe_print_unified_clap_subcommand_help(argv: &[String]) -> bool {

while index < argv.len() {
let arg = &argv[index];
if is_help_flag(arg) {
index += 1;
continue;
}
if arg.starts_with('-') {
break;
}
Expand All @@ -999,6 +1013,10 @@ pub fn maybe_print_unified_clap_subcommand_help(argv: &[String]) -> bool {
return false;
}

if should_skip_parent_help_for_unknown_direct_nested_child(&command_path, argv, index) {
return false;
}

let Some(first_command_name) = first_command_name else {
return false;
};
Expand All @@ -1008,7 +1026,7 @@ pub fn maybe_print_unified_clap_subcommand_help(argv: &[String]) -> bool {

// Respect `--` option terminator: flags after `--` belong to the wrapped
// command and should not trigger CLI help rewriting.
if !has_help_flag_before_terminator(&argv[index..]) {
if !has_help_flag_before_terminator(&argv[1..]) {
return false;
}

Expand Down Expand Up @@ -1083,7 +1101,8 @@ pub fn print_unified_clap_help_for_path(command_path: &[&str]) -> bool {
mod tests {
use super::{
HelpDoc, documentation_url_for_command_path, has_help_flag_before_terminator,
parse_clap_help_to_doc, parse_rows, render_help_doc, split_comment_suffix, strip_ansi,
parse_clap_help_to_doc, parse_rows, render_help_doc,
should_skip_parent_help_for_unknown_direct_nested_child, split_comment_suffix, strip_ansi,
};

#[test]
Expand Down Expand Up @@ -1134,6 +1153,42 @@ Options:
assert!(!has_help_flag_before_terminator(&args));
}

#[test]
fn skips_parent_help_for_unknown_pm_child_with_help() {
let args = vec!["vp", "pm", "apprev-build", "--help"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
assert!(should_skip_parent_help_for_unknown_direct_nested_child(
&["pm".to_string()],
&args,
2,
));
}

#[test]
fn keeps_unified_help_for_valid_pm_child_with_help() {
let args = vec!["vp", "pm", "approve-builds", "--help"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
assert!(!should_skip_parent_help_for_unknown_direct_nested_child(
&["pm".to_string(), "approve-builds".to_string()],
&args,
3,
));
}

#[test]
fn keeps_unified_help_for_parent_help() {
let args = vec!["vp", "env", "--help"].into_iter().map(String::from).collect::<Vec<_>>();
assert!(!should_skip_parent_help_for_unknown_direct_nested_child(
&["env".to_string()],
&args,
2,
));
}

#[test]
fn strip_ansi_removes_csi_sequences() {
let input = "\u{1b}[1mOptions:\u{1b}[0m";
Expand Down
89 changes: 76 additions & 13 deletions crates/vite_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use crate::cli::{
/// Normalize CLI arguments:
/// - `vp list ...` / `vp ls ...` → `vp pm list ...`
/// - `vp rebuild ...` → `vp pm rebuild ...`
/// - `vp help [command]` → `vp [command] --help`
/// - `vp help [command] [args...]` → `vp [command] [args...] --help`
/// - `vp node [args...]` → `vp env exec node [args...]`
fn normalize_args(args: Vec<String>) -> Vec<String> {
let mut normalized = args;
Expand Down Expand Up @@ -71,13 +71,26 @@ fn normalize_args(args: Vec<String>) -> Vec<String> {
Some("help") if normalized.len() == 2 => {
vec![normalized[0].clone(), "--help".to_string()]
}
// `vp help [command] [args...]` -> `vp [command] --help [args...]`
// `vp help [command] [args...]` -> `vp [command] [args...] --help`
Some("help") if normalized.len() > 2 => {
let mut next = Vec::with_capacity(normalized.len());
next.push(normalized[0].clone());
next.push(normalized[2].clone());
next.extend(normalized[2..].iter().cloned());
next.push("--help".to_string());
next.extend(normalized[3..].iter().cloned());
next
}
// `vp pm --help <command>` → `vp pm <command> --help`
// `vp env --help <command>` → `vp env <command> --help`
Some("pm" | "env")
if normalized.get(2).is_some_and(|arg| matches!(arg.as_str(), "-h" | "--help"))
&& normalized.get(3).is_some_and(|arg| !arg.starts_with('-')) =>
{
let mut next = Vec::with_capacity(normalized.len());
next.push(normalized[0].clone());
next.push(normalized[1].clone());
next.push(normalized[3].clone());
next.push(normalized[2].clone());
next.extend(normalized[4..].iter().cloned());
next
}
// `vp node [args...]` → `vp env exec node [args...]`
Expand Down Expand Up @@ -127,6 +140,26 @@ fn print_invalid_subcommand_error(details: &InvalidSubcommandDetails) {
output::error(&format!("Command '{highlighted_subcommand}' not found"));
}

fn print_nested_suggestion(suggestion: &str) {
eprintln!();
let highlighted_suggestion = format!("`vp {suggestion}`").bright_blue().to_string();
eprintln!("Did you mean {highlighted_suggestion}?");
}

fn nested_suggestion_command(
raw_args: &[String],
invalid_subcommand: &str,
suggestion: &str,
) -> String {
let Some(index) = raw_args.iter().position(|arg| arg == invalid_subcommand) else {
return suggestion.to_owned();
};

let mut corrected = raw_args[..index].to_vec();
corrected.push(suggestion.to_owned());
corrected.join(" ")
}

fn is_affirmative_response(input: &str) -> bool {
matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes" | "ok" | "true" | "1")
}
Expand Down Expand Up @@ -319,6 +352,7 @@ async fn main() -> ExitCode {

// Normalize arguments (list/ls aliases, help rewriting)
let normalized_args = normalize_args(args);
let normalized_raw_args = normalized_args.get(1..).map_or_else(Vec::new, |args| args.to_vec());

// Print unified subcommand help for clap-managed commands before clap handles help output.
if help::maybe_print_unified_clap_subcommand_help(&normalized_args) {
Expand Down Expand Up @@ -346,18 +380,33 @@ async fn main() -> ExitCode {
ExitCode::SUCCESS
} else if matches!(e.kind(), ErrorKind::InvalidSubcommand) {
if let Some(details) = extract_invalid_subcommand_details(&e) {
let corrected_top_level_args =
details.suggestion.as_ref().and_then(|suggestion| {
replace_top_level_typoed_subcommand(
&raw_args,
&details.invalid_subcommand,
suggestion,
)
});

print_invalid_subcommand_error(&details);

if let Some(suggestion) = &details.suggestion
&& let Some(corrected_raw_args) = replace_top_level_typoed_subcommand(
&raw_args,
&details.invalid_subcommand,
suggestion,
)
&& prompt_to_run_suggested_command(suggestion)
{
run_corrected_args(&cwd, &corrected_raw_args).await
if let Some(corrected_raw_args) = corrected_top_level_args {
let suggestion = details.suggestion.as_ref().expect("suggestion exists");
if prompt_to_run_suggested_command(suggestion) {
run_corrected_args(&cwd, &corrected_raw_args).await
} else {
clap_error_to_exit_code(&e)
}
} else {
if let Some(suggestion) = &details.suggestion {
let suggestion = nested_suggestion_command(
&normalized_raw_args,
&details.invalid_subcommand,
suggestion,
);
print_nested_suggestion(&suggestion);
}
clap_error_to_exit_code(&e)
}
} else {
Expand Down Expand Up @@ -453,6 +502,20 @@ mod tests {
assert_eq!(normalized, s(&["vp", "env", "exec", "node", "--help"]));
}

#[test]
fn normalize_args_keeps_help_after_nested_path() {
let input = s(&["vp", "help", "pm", "apprev-build"]);
let normalized = normalize_args(input);
assert_eq!(normalized, s(&["vp", "pm", "apprev-build", "--help"]));
}

#[test]
fn normalize_args_moves_pm_help_before_child_after_child() {
let input = s(&["vp", "pm", "--help", "apprev-build"]);
let normalized = normalize_args(input);
assert_eq!(normalized, s(&["vp", "pm", "apprev-build", "--help"]));
}

#[test]
fn unknown_argument_detected_without_pass_as_value_hint() {
let error = try_parse_args_from(["vp".to_string(), "--cache".to_string()])
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/snap-tests-global/command-nested-typo-help/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[2]> vp pm apprev-build --help # typo should not print pm parent help
error: Command 'apprev-build' not found

Did you mean `vp pm approve-builds`?

[2]> vp help pm apprev-build # help alias should not print pm parent help for a typo
error: Command 'apprev-build' not found

Did you mean `vp pm approve-builds`?

[2]> vp pm --help apprev-build # help flag before typo should not print pm parent help
error: Command 'apprev-build' not found

Did you mean `vp pm approve-builds`?
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"env": {},
"commands": [
"vp pm apprev-build --help # typo should not print pm parent help",
"vp help pm apprev-build # help alias should not print pm parent help for a typo",
"vp pm --help apprev-build # help flag before typo should not print pm parent help"
]
}
Loading