diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 1a120f633abc..36a3ac2c76b4 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -36,9 +36,7 @@ use crate::commands::pip::operations; use crate::commands::project::{resolve_names, EnvironmentSpecification, ProjectError}; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::tool::Target; -use crate::commands::{ - project::environment::CachedEnvironment, tool::common::matching_packages, tool_list, -}; +use crate::commands::{project::environment::CachedEnvironment, tool::common::matching_packages}; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -79,9 +77,13 @@ pub(crate) async fn run( cache: Cache, printer: Printer, ) -> anyhow::Result { - // treat empty command as `uv tool list` + // Treat empty command similar to `uv tool list`, list available tools. let Some(command) = command else { - return tool_list(false, false, &cache, printer).await; + match list_available_tools(invocation_source, &cache, printer).await { + // It is a failure because user misses a required tool name. + Ok(()) => return Ok(ExitStatus::Error), + Err(err) => return Err(err), + }; }; let (target, args) = command.split(); @@ -262,6 +264,77 @@ fn get_entrypoints( )?) } +/// Display a list of tools that provide the executable. +/// +/// If there is no package providing the executable, we will display a message to how to install a package. +async fn list_available_tools( + invocation_source: ToolRunCommand, + cache: &Cache, + printer: Printer, +) -> anyhow::Result<()> { + writeln!( + printer.stdout(), + "Provide a command to invoke with `{invocation_source} ` \ + or `{invocation_source} --from `.\n" + )?; + + let installed_tools = InstalledTools::from_settings()?; + let no_tools_installed_msg = + "No tools installed. See `uv tool install --help` for more information."; + let _lock = match installed_tools.lock().await { + Ok(lock) => lock, + Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + writeln!(printer.stdout(), "{no_tools_installed_msg}")?; + return Ok(()); + } + Err(err) => return Err(err.into()), + }; + + let mut tools = installed_tools.tools()?.into_iter().collect::>(); + tools.sort_by_key(|(name, _)| name.clone()); + + if tools.is_empty() { + writeln!(printer.stdout(), "{no_tools_installed_msg}")?; + return Ok(()); + } + + let mut buf = String::new(); + for (name, tool) in tools { + // Skip invalid tools. + let Ok(tool) = tool else { + continue; + }; + + // Output tool name and version. + let Ok(version) = installed_tools.version(&name, cache) else { + continue; + }; + writeln!(buf, "{}", format!("{name} v{version}").bold())?; + + // Output tool entrypoints. + for entrypoint in tool.entrypoints() { + writeln!(buf, "- {}", entrypoint.name)?; + } + } + + // Installed tools were malformed or failed fetching versions. + if buf.is_empty() { + writeln!(printer.stderr(), "{no_tools_installed_msg}")?; + return Ok(()); + } + + writeln!( + printer.stdout(), + "The following tools are already installed:\n" + )?; + writeln!(printer.stdout(), "{buf}")?; + writeln!( + printer.stdout(), + "See `{invocation_source} --help` for more information." + )?; + Ok(()) +} + /// Display a warning if an executable is not provided by package. /// /// If found in a dependency of the requested package instead of the requested package itself, we will hint to use that instead. diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index d92e5e5beec2..bc8445b4f48b 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -750,12 +750,14 @@ fn tool_run_list_installed() { uv_snapshot!(context.filters(), context.tool_run() .env("UV_TOOL_DIR", tool_dir.as_os_str()) .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- + Provide a command to invoke with `uv tool run ` or `uv tool run --from `. + + No tools installed. See `uv tool install --help` for more information. ----- stderr ----- - No tools installed "###); // Install `black`. @@ -771,13 +773,19 @@ fn tool_run_list_installed() { uv_snapshot!(context.filters(), context.tool_run() .env("UV_TOOL_DIR", tool_dir.as_os_str()) .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- + Provide a command to invoke with `uv tool run ` or `uv tool run --from `. + + The following tools are already installed: + black v24.2.0 - black - blackd + See `uv tool run --help` for more information. + ----- stderr ----- "###); } @@ -891,9 +899,8 @@ fn tool_run_with_editable() -> anyhow::Result<()> { "###); // Requesting an editable requirement should install it in a layer, even if it satisfied - uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg("./src/anyio_local").arg("flask").arg("--version").env("UV_TOOL_DIR", tool_dir.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str()) - - , @r###" + uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg("./src/anyio_local").arg("flask").arg("--version").env("UV_TOOL_DIR", tool_dir.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str()), + @r###" success: true exit_code: 0 ----- stdout -----