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
18 changes: 18 additions & 0 deletions e2e/cli/test_unknown_command_suggestion
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Test that mistyped CLI commands suggest the correct command instead of
# showing a confusing "no tasks defined" error.
# See: https://github.com/jdx/mise/discussions/8278

set -euo pipefail
export RUST_BACKTRACE=0
export MISE_FRIENDLY_ERROR=1

# Test: Typo of a known command should suggest the correct command
assert_fail "mise bin-path" "Did you mean"
assert_fail "mise bin-path" "bin-paths"

# Test: Another typo should suggest the correct command
assert_fail "mise instal" "install"

# Test: Should say "unknown command" not "no tasks defined" for command typos
assert_fail "mise bin-path" "unknown command"
40 changes: 40 additions & 0 deletions src/task/task_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,42 @@ fn validate_monorepo_setup(config: &Arc<Config>) -> Result<()> {
Ok(())
}

/// Check if a name is similar to any known CLI subcommands using fuzzy matching
fn suggest_similar_commands(name: &str) -> Vec<String> {
use clap::CommandFactory;
let cmd = crate::cli::Cli::command();
let matcher = SkimMatcherV2::default().use_cache(true).smart_case();
cmd.get_subcommands()
.flat_map(|s| std::iter::once(s.get_name()).chain(s.get_all_aliases()))
Comment on lines +69 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is recommended to filter out hidden subcommands from the suggestions. Internal or plumbing commands (such as hook-env, completion, etc.) are typically not intended for direct use and can make the suggestions less relevant to the user's intent.

    cmd.get_subcommands()
        .filter(|s| !s.is_hide_set())
        .flat_map(|s| std::iter::once(s.get_name()).chain(s.get_all_aliases()))

Comment on lines +66 to +70

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest_similar_commands() rebuilds the full clap Cli::command() and a new SkimMatcherV2 on every error. Since this runs on the error path but can still be hit frequently (e.g., repeated task typos in scripts), consider caching the subcommand/alias list and/or the matcher in a static LazyLock similar to other fuzzy-match usage in the repo.

Copilot uses AI. Check for mistakes.
.filter_map(|subcmd| {
matcher
.fuzzy_match(subcmd, name)
.filter(|&score| score > 0)
.map(|score| (score, subcmd.to_string()))
})
.sorted_by_key(|(score, _)| -1 * *score)
.take(3)
Comment on lines +76 to +78

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting via -1 * *score works but is brittle (potential overflow on i64::MIN) and less readable. Prefer sorting descending via std::cmp::Reverse(*score) (or an explicit comparator) for clarity and safety.

Copilot uses AI. Check for mistakes.
.map(|(_, subcmd)| subcmd)
.collect()
}

/// Show an error when a task is not found, with helpful suggestions
async fn err_no_task(config: &Config, name: &str) -> Result<()> {
// Check early if the name looks like a mistyped CLI subcommand
let similar_cmds = suggest_similar_commands(name);

if config.tasks().await.is_ok_and(|t| t.is_empty()) {
// If the name matches a CLI subcommand closely, suggest that instead of
// the confusing "no tasks defined" message
if !similar_cmds.is_empty() {
let mut err_msg = format!("unknown command: {}", style::ered(name));
err_msg.push_str("\n\nDid you mean:");

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion header here is Did you mean: (colon) while other suggestion messages in the codebase use a question mark (e.g., Did you mean? / Did you mean one of these?). Consider aligning the punctuation/wording for consistency, especially since tests/searches may rely on the exact phrasing.

Suggested change
err_msg.push_str("\n\nDid you mean:");
err_msg.push_str("\n\nDid you mean?");

Copilot uses AI. Check for mistakes.
for cmd_name in &similar_cmds {
err_msg.push_str(&format!("\n mise {cmd_name}"));
}
bail!(err_msg);
}
Comment on lines +89 to +98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The check for similar CLI commands currently takes precedence over the check for untrusted configuration files. If a user has tasks defined in an untrusted config but mistypes the task name (or if the name fuzzy-matches a CLI command), they will see an 'unknown command' error instead of being prompted to trust their config.

It is better to prioritize the untrusted config check, as this is a common reason why expected tasks are missing from the task list.


// Check if there are any untrusted config files in the current directory
// that might contain tasks
if let Some(cwd) = &*dirs::CWD {
Expand Down Expand Up @@ -149,6 +182,13 @@ async fn err_no_task(config: &Config, name: &str) -> Result<()> {
}
}

if !similar_cmds.is_empty() {
err_msg.push_str("\n\nDid you mean the command:");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the error message used when no tasks are defined (line 93), consider using the simpler 'Did you mean:' prompt here as well.

        err_msg.push_str("\n\nDid you mean:");

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header says Did you mean the command: but multiple suggestions can be printed. Consider making this consistent with the earlier branch (and pluralizing if needed), e.g. use the same Did you mean… header in both places to avoid two different formats for the same kind of suggestion.

Suggested change
err_msg.push_str("\n\nDid you mean the command:");
err_msg.push_str("\n\nDid you mean one of these?");

Copilot uses AI. Check for mistakes.
for cmd_name in &similar_cmds {
err_msg.push_str(&format!("\n mise {cmd_name}"));
}
}

bail!(err_msg);
}

Expand Down
Loading