| File: Utils\NpmInstallDetection.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> /// Detects whether the Aspire CLI is running from a global npm install of the /// <c>@microsoft/aspire-cli</c> package and provides the npm-equivalent /// self-update command so the CLI surfaces the correct guidance instead of /// attempting to overwrite npm-owned files with the GitHub-binary downloader. /// </summary> /// <remarks> /// The npm launcher (<c>eng/clipack/npm/aspire.js</c>) sets three environment /// variables when it spawns the native CLI binary: <c>ASPIRE_NPM_PACKAGE</c>, /// <c>ASPIRE_NPM_PACKAGE_VERSION</c>, and <c>ASPIRE_NPM_PACKAGE_RID</c>. /// Presence of <c>ASPIRE_NPM_PACKAGE</c> with the expected package name is /// treated as the authoritative signal that the CLI was launched by the npm /// launcher; the other variables are surfaced via accessors for diagnostics. /// </remarks> internal static class NpmInstallDetection { internal const string PackageEnvironmentVariableName = "ASPIRE_NPM_PACKAGE"; internal const string PackageVersionEnvironmentVariableName = "ASPIRE_NPM_PACKAGE_VERSION"; internal const string PackageRidEnvironmentVariableName = "ASPIRE_NPM_PACKAGE_RID"; internal const string ExpectedPackageName = "@microsoft/aspire-cli"; private static readonly AsyncLocal<IEnvironmentReader?> s_environmentOverride = new(); internal static bool IsRunningFromNpm() { return GetNpmUpdateCommand() is not null; } internal static string? GetNpmUpdateCommand() { var env = s_environmentOverride.Value ?? ProcessEnvironmentReader.Instance; var packageName = env.GetEnvironmentVariable(PackageEnvironmentVariableName); if (string.IsNullOrWhiteSpace(packageName)) { return null; } // The launcher always writes the canonical "@microsoft/aspire-cli" package name. // Reject anything else so an unrelated env var collision does not flip the CLI // into the npm self-update path. if (!string.Equals(packageName, ExpectedPackageName, StringComparison.Ordinal)) { return null; } return $"npm install -g {ExpectedPackageName}@latest"; } internal static string? GetNpmPackageVersion() { var env = s_environmentOverride.Value ?? ProcessEnvironmentReader.Instance; var version = env.GetEnvironmentVariable(PackageVersionEnvironmentVariableName); return string.IsNullOrWhiteSpace(version) ? null : version; } internal static string? GetNpmPackageRid() { var env = s_environmentOverride.Value ?? ProcessEnvironmentReader.Instance; var rid = env.GetEnvironmentVariable(PackageRidEnvironmentVariableName); return string.IsNullOrWhiteSpace(rid) ? null : rid; } internal static IDisposable UseEnvironmentForTesting(IReadOnlyDictionary<string, string?> environment) { var previous = s_environmentOverride.Value; s_environmentOverride.Value = new DictionaryEnvironmentReader(environment); return new EnvironmentOverrideScope(previous); } internal interface IEnvironmentReader { string? GetEnvironmentVariable(string name); } private sealed class ProcessEnvironmentReader : IEnvironmentReader { public static readonly ProcessEnvironmentReader Instance = new(); public string? GetEnvironmentVariable(string name) { return Environment.GetEnvironmentVariable(name); } } private sealed class DictionaryEnvironmentReader(IReadOnlyDictionary<string, string?> environment) : IEnvironmentReader { public string? GetEnvironmentVariable(string name) { return environment.TryGetValue(name, out var value) ? value : null; } } private sealed class EnvironmentOverrideScope(IEnvironmentReader? previous) : IDisposable { public void Dispose() { s_environmentOverride.Value = previous; } } }