Skip to content

Add a one-shot execution mode for .NET tools a la npx #31103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
baronfel opened this issue Mar 9, 2023 · 14 comments · May be fixed by #48443
Open

Add a one-shot execution mode for .NET tools a la npx #31103

baronfel opened this issue Mar 9, 2023 · 14 comments · May be fixed by #48443
Assignees

Comments

@baronfel
Copy link
Member

baronfel commented Mar 9, 2023

Is your feature request related to a problem? Please describe.

Currently to use a .NET global tool that may or may not be installed, you need to perform a series of steps

  • install the tool globally
  • ensure the global tool install directory is on the PATH
  • invoke the tool
  • (optionally) remove the tool

And for local tools, you need to

  • create a tool manifest
  • install the tool
  • invoke the tool
  • (optionally) remove the tool

When all you really want to do is

  • invoke the tool

We should consider adding a mode of running a .NET tool that combines all of these steps into one user-facing step.

Describe the solution you'd like

Something akin to

dotnet toolx [-g|--global] [--rm] [--version toolVersion] <toolName> [tool args and options]

that would combine all of the preceding steps into one logical command. The tool would be installed (locally by default, creating a manifest if necessary), executed passing along whatever args, and then optionally being uninstalled after execution. If a version was supplied and the locally-available version of the tool (if any) doesn't satisfy that version, the specified version would be installed.

Additional context

Add any other context or screenshots about the feature request here.

@ghost ghost added the untriaged Request triage from a team member label Mar 9, 2023
@baronfel baronfel changed the title Add a one-shot execution mode for .NET tools Add a one-shot execution mode for .NET tools a la npx Mar 28, 2025
@baronfel
Copy link
Member Author

baronfel commented Apr 3, 2025

Requirements

  • There should be one command to install, execute, and clean up a tool
  • There should be no artifacts on the PATH or other user system side effects as a result of using the new command
  • If a tool is run without any version, the latest version available on the configured feeds should be used
  • For performance, if the tool version is already downloaded locally, the local download should be used
  • For security, we should prompt before downloading a package that doesn't exist locally
    • We should accept a -y/-n style argument to prevent blocking
  • For security, we should allow users to use the existing NuGet source-management commands to manage the feeds that are used to do version-searching and package downloads
  • The happy path should be to do the most recent, awesome thing possible in the absence of any CLI args overriding that choice
    • latest version
    • most recent TFM/arch
    • quietest verbosity
    • etc

CLI Interface

In general, we should reuse as many relevant flags from the existing tool install commands as possible.

Keep

  • --version (though this is maybe not required if a user uses the package@version syntax)
  • --configfile (for controlling NuGet configuration)
  • --add-source / --source (for controlling NuGet feeds)
  • --verbosity (all commands should support verbosity)
  • --prerelease (allow using prerelease packages when no version is provided)
  • --ignore-failed-sources (though for usability we could assume this by default)
  • --interactive (though this is on by default in newer SDKs)

Consider

  • --allow-roll-forward (we may want to do this by default for toolx)
  • --framework (which TFM to use - we should use TFM of the current SDK)
  • --arch (we should use the arch of the current SDK)

Ignore

  • --create-manifest-if-needed
  • --allow-downgrade
  • --disable-parallel
  • --no-http-cache
  • --global
  • --local
  • --tool-path
  • --tool-manifest-path

Process

When a user runs dotnet toolx fsautocomplete, we should

  • check the feed configuration and scrape the feeds for the latest version of fsautocomplete
  • see if that package is available in the configured global package caches
    • if so, invoke the fsautocomplete tool
    • if not, download that package from the configured feeds, then invoke that tool

Invoking the tool

There are two main alternatives here

  • dotnet-based invocation
  • apphost invocation

They have their pros and cons. If we do dotnet-based invocation then there's less work to do and we don't have to worry about where to store the apphosts to prevent cluttering PATH, etc.
If we do apphost-based invocation then users may get a clearer chain of responsibility - you can see the apphost, etc.

@timheuer
Copy link
Member

timheuer commented Apr 7, 2025

what about --skip-failed-sources I feel this always trips up some scenarios if we don't think about it

@baronfel baronfel removed the untriaged Request triage from a team member label Apr 11, 2025
@baronfel
Copy link
Member Author

@timheuer --ignore-failed-sources is one of the ones we'd keep, yeah. It's in my 'Keep' list :)

@nagilson
Copy link
Member

nagilson commented Apr 16, 2025

I would support adding a 'latest' option if you go the path of preferring local (any version) -> global (any version) -> downloading (latest) to the temp folder (cached), if no version is specified. I would also support adding a version and respecting that, as well as doing tool@version.

@nagilson
Copy link
Member

Shorthand suggestions from @dsplaisted: dnx or dntx

@dsplaisted
Copy link
Member

Have we thought about how to treat the difference between the tool package name and the command name? I think it's common for those to be different, for example:

Package Id                        Version         Commands
--------------------------------------------------------------------
microsoft.dotnet-interactive      1.0.415202      dotnet-interactive
powershell                        7.5.0           pwsh

Today, you use the package name to install the tool, but the command name to invoke it. How should we handle this with one-shot execution mode?

@baronfel @edvilme

@edvilme
Copy link
Contributor

edvilme commented Apr 17, 2025

Have we thought about how to treat the difference between the tool package name and the command name? I think it's common for those to be different, for example:

Package Id                        Version         Commands
--------------------------------------------------------------------
microsoft.dotnet-interactive      1.0.415202      dotnet-interactive
powershell                        7.5.0           pwsh

Today, you use the package name to install the tool, but the command name to invoke it. How should we handle this with one-shot execution mode?

@baronfel @edvilme

The way it currently works on my pr is like that. We use the package name to install, and the command name (retrieved from the package data) to execute.
However, since this is still the dotnet tool run command, I see why people would be confused to put in the package name instead of the command

@baronfel
Copy link
Member Author

There are a few options:

  • if the package contains only a single tool, then invoke that tool
  • do like npx does and provide a way to specify the package and command separately: npx --package=<name[@version]> <cmd> <args>

@dsplaisted
Copy link
Member

We currently only support one command per tool package. But it seems like it will be confusing to have to use pwsh or dotnet tool run pwsh for a global or local tool, but to have to do something like dotnet tool run powershell or dnx powershell for the one-shot mode.

I think it also means that the idea of automatically using one-shot mode (with prompting) if the specified tool isn't installed locally or globally doesn't make as much sense, because the names won't match.

Do npm and uv support a different command name from the package name, or do they always have to match?

Ideas to solve:

  • Have a mapping from the command name to the package name. This would be kind of complicated, we'd probably end up with a .NET Tool indexer similar to what we have for template search, which would mean this would only work well for tools on NuGet.org.
  • Have conventions for how command names and tool package names should be related. We could say that the package name should be the same as the command name, or maybe that the package name should be the command name prefixed by dotnet- or some other convention.

@edvilme
Copy link
Contributor

edvilme commented Apr 17, 2025

If we're doing this inside the run command, I think it is important to try and prioritize the command name over the package name for those reasons. I am curious if we could do something like:

dotnet tool run <command> --from-source-package=<name[@version]> <args>

That way, we respect the current behavior of the run command while at the same time explicitly specifying the package name to be run in one-shot mode. We can even make the command argument optional when passed the --from-source-package option so it defaults to the package command.

@dsplaisted
Copy link
Member

I think we want to try to make it so that people don't have to think about the difference between the tool package name and the command name. I think we could probably have a prioritized list of conventions we follow to look up a package name based on the command name. Browsing through the tool packages on NuGet, we might use some of the following conventions for the package name:

  • <CommandName>
  • dotnet-<CommandName>
  • <CommandName>.Tool
  • <CommandName>.DotNetTool

@tmds
Copy link
Member

tmds commented Apr 18, 2025

Probing different names allows someone to hijack a name (or introduce ambiguity).

@jeffkl
Copy link
Contributor

jeffkl commented Apr 21, 2025

I think dotnet tool exec should have the same arguments as dotnet tool install, with the only difference is that the tool runs after installation. That way I don't have to change any command-line arguments, just change install to exec. And I also think that command arguments should come after a final -- which is how I've seen other commands work.

dotnet tool exec --global Microsoft.VisualStudio.SlnGen.Tool -- --launch:false

@edvilme
Copy link
Contributor

edvilme commented Apr 22, 2025

I think dotnet tool exec should have the same arguments as dotnet tool install, with the only difference is that the tool runs after installation. That way I don't have to change any command-line arguments, just change install to exec. And I also think that command arguments should come after a final -- which is how I've seen other commands work.

dotnet tool exec --global Microsoft.VisualStudio.SlnGen.Tool -- --launch:false

I agree mostly but am not sure about the --global option being present.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants