| File: Utils\CommandPathResolver.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Cli.Utils; /// <summary> /// Resolves commands from PATH and produces actionable error messages when they are missing. /// </summary> internal static class CommandPathResolver { private static readonly Dictionary<string, CommandMetadata> s_commandMetadata = new(StringComparer.OrdinalIgnoreCase) { ["npm"] = new("Node.js", "https://nodejs.org/en/download"), ["npx"] = new("Node.js", "https://nodejs.org/en/download"), ["bun"] = new("Bun", "https://bun.sh/docs/installation"), ["yarn"] = new("Yarn", "https://yarnpkg.com/getting-started/install"), ["pnpm"] = new("pnpm", "https://pnpm.io/installation") }; /// <summary> /// Resolves a command from the system PATH. /// </summary> /// <param name="command">The command to resolve.</param> /// <param name="resolvedCommand">The resolved command path when found.</param> /// <param name="errorMessage">The user-facing error message when the command is missing.</param> /// <returns><see langword="true"/> when the command is found; otherwise, <see langword="false"/>.</returns> public static bool TryResolveCommand(string command, out string? resolvedCommand, out string? errorMessage) { return TryResolveCommand(command, PathLookupHelper.FindFullPathFromPath, out resolvedCommand, out errorMessage); } /// <summary> /// Resolves a command from a custom lookup source. /// </summary> /// <param name="command">The command to resolve.</param> /// <param name="commandResolver">The resolver used to find the command.</param> /// <param name="resolvedCommand">The resolved command path when found.</param> /// <param name="errorMessage">The user-facing error message when the command is missing.</param> /// <returns><see langword="true"/> when the command is found; otherwise, <see langword="false"/>.</returns> internal static bool TryResolveCommand( string command, Func<string, string?> commandResolver, out string? resolvedCommand, out string? errorMessage) { resolvedCommand = commandResolver(command); if (resolvedCommand is not null) { errorMessage = null; return true; } errorMessage = GetMissingCommandMessage(command); return false; } /// <summary> /// Gets a user-facing error message for a missing command. /// </summary> /// <param name="command">The missing command.</param> /// <returns>An actionable error message.</returns> internal static string GetMissingCommandMessage(string command) { var normalizedCommand = NormalizeCommand(command); if (s_commandMetadata.TryGetValue(normalizedCommand, out var metadata)) { return $"{normalizedCommand} is not installed or not found in PATH. Please install {metadata.InstallDisplayName} and try again."; } return $"Command '{command}' not found. Please ensure it is installed and in your PATH."; } internal static string? GetInstallationLink(string command) { return s_commandMetadata.TryGetValue(NormalizeCommand(command), out var metadata) ? metadata.InstallationLink : null; } // RunCommand comes from MSBuild as a command string, not a ProcessStartInfo executable path. // For the default SDK targets it is either the literal "dotnet" or a generated apphost path, // but custom targets can quote the path. Normalize only enough to decide whether to replace a // dotnet muxer command with the CLI's resolved SDK muxer. internal static string NormalizeRunCommand(string command) => command.Trim().Trim('"'); private static string NormalizeCommand(string command) { return Path.GetFileNameWithoutExtension(command); } private sealed record CommandMetadata(string InstallDisplayName, string InstallationLink); }