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
51 changes: 51 additions & 0 deletions e2e/cli/test_hook_not_found_auto_install
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash

# Test that not_found_auto_install works when a tool is already configured but not installed
# Reproduces https://github.com/jdx/mise/discussions/6482#discussioncomment-14552091

# Test 1: hook-not-found auto-install
# Set up: Install tiny@3.1.0 globally
mise use -g tiny@3.1.0
assert "mise current tiny" "3.1.0"

# Create a project that requires a different version (tiny@3.0.0)
cat >mise.toml <<EOF
[tools]
tiny = "3.0.0"
EOF

# Uninstall the required version to simulate it being missing
mise uninstall tiny@3.0.0 || true

# Verify it's not installed
assert_fail "mise ls tiny --installed | grep 3.0.0"

# Simulate the hook-not-found behavior: rtx-tiny is requested but not installed
# This should auto-install tiny@3.0.0 (note: tiny plugin provides rtx-tiny binary)
mise hook-not-found rtx-tiny && mise current tiny | grep -q "3.0.0"
assert_contains "mise ls tiny --installed" "3.0.0"

# Clean up for next test
rm -f mise.toml
mise uninstall tiny@3.0.0

# Test 2: shim auto-install
# Set up: Install tiny@3.1.0 globally, configure tiny@3.0.0 locally (not installed)
cat >mise.toml <<EOF
[tools]
tiny = "3.0.0"
EOF

# Verify it's not installed
assert_fail "mise ls tiny --installed | grep 3.0.0"

# Add shims to PATH and run the shimmed binary
# This should auto-install tiny@3.0.0 via the shim
export PATH="$MISE_DATA_DIR/shims:$PATH"
rtx-tiny && mise current tiny | grep -q "3.0.0"
assert_contains "mise ls tiny --installed" "3.0.0"

# Clean up
rm -f mise.toml
mise use -g --rm tiny
mise uninstall tiny --all
2 changes: 1 addition & 1 deletion src/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async fn which_shim(config: &mut Arc<Config>, bin_name: &str) -> Result<PathBuf>
return Ok(bin);
}
}
if Settings::get().not_found_auto_install && console::user_attended() {
if Settings::get().not_found_auto_install {
for tv in ts
.install_missing_bin(config, bin_name)
.await?
Expand Down
31 changes: 31 additions & 0 deletions src/toolset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,12 +860,43 @@ impl Toolset {
config: &mut Arc<Config>,
bin_name: &str,
) -> Result<Option<Vec<ToolVersion>>> {
// Strategy: Find backends that could provide this bin by checking:
// 1. Any currently installed versions that provide the bin
// 2. Any requested backends with installed versions (even if not current)
let mut plugins = IndexSet::new();

// First check currently active installed versions
for (p, tv) in self.list_current_installed_versions(config) {
if let Ok(Some(_bin)) = p.which(config, &tv, bin_name).await {
plugins.insert(p);
}
}

// Also check backends that are requested but not currently active
// This handles the case where a user has tool@v1 globally and tool@v2 locally (not installed)
// When looking for a bin provided by the tool, we check if any installed version provides it
let all_installed = self.list_installed_versions(config).await?;
for (backend, _versions) in self.list_versions_by_plugin() {
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

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

The list_installed_versions call is made for every iteration of the loop, but it's called outside the loop. However, the loop iterates over all backends and calls list_versions_by_plugin() which could be expensive. Consider caching the result or restructuring to avoid redundant lookups if this method is called frequently.

Suggested change
for (backend, _versions) in self.list_versions_by_plugin() {
let all_versions_by_plugin = self.list_versions_by_plugin();
for (backend, _versions) in all_versions_by_plugin {

Copilot uses AI. Check for mistakes.
// Skip if we already found this backend
if plugins.contains(&backend) {
continue;
}

// Check if this backend has ANY installed version that provides the bin
let backend_versions: Vec<_> = all_installed
.iter()
.filter(|(p, _)| p.ba() == backend.ba())
.collect();

for (_, tv) in backend_versions {
if let Ok(Some(_bin)) = backend.which(config, tv, bin_name).await {
plugins.insert(backend.clone());
break;
}
}
}

// Install missing versions for backends that provide this bin
for plugin in plugins {
let versions = self
.list_missing_versions(config)
Expand Down
Loading