File: Commands\TerminalResourceResolver.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.
 
using System.Globalization;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Resolves resource names and replicas consistently for interactive and scripted terminal commands.
/// </summary>
internal sealed class TerminalResourceResolver(IInteractionService interactionService)
{
    public async Task<(string ResourceName, TerminalReplicaInfo? Replica)> ResolveAsync(
        IAppHostAuxiliaryBackchannel connection,
        string resourceName,
        int? requestedReplica,
        CancellationToken cancellationToken)
    {
        var snapshots = await interactionService.ShowStatusAsync(
            TerminalCommandStrings.LookingUpResource,
            async () => await connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false));
 
        var matches = ResourceSnapshotMapper.WhereMatchesResourceName(snapshots, resourceName).ToList();
        if (matches.Count == 0)
        {
            interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture,
                TerminalCommandStrings.ResourceNotFound, resourceName));
            return (resourceName, null);
        }
 
        // Replicas share the parent DisplayName carrying WithTerminal(), rather than their individual names.
        var canonicalName = !string.IsNullOrEmpty(matches[0].DisplayName)
            ? matches[0].DisplayName!
            : matches[0].Name;
        var info = await interactionService.ShowStatusAsync(
            TerminalCommandStrings.DiscoveringSessions,
            async () => await connection.GetTerminalInfoAsync(canonicalName, cancellationToken).ConfigureAwait(false));
 
        if (!info.IsAvailable || info.Replicas is not { Length: > 0 } replicas)
        {
            interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture,
                TerminalCommandStrings.TerminalUnavailable, canonicalName));
            return (canonicalName, null);
        }
 
        if (requestedReplica is { } index)
        {
            var match = Array.Find(replicas, r => r.ReplicaIndex == index);
            if (match is null)
            {
                interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture,
                    TerminalCommandStrings.ReplicaNotFound, index, canonicalName,
                    string.Join(", ", replicas.Select(r => r.ReplicaIndex.ToString(CultureInfo.InvariantCulture)))));
            }
            return (canonicalName, match);
        }
 
        if (replicas.Length == 1)
        {
            return (canonicalName, replicas[0]);
        }
 
        if (Console.IsInputRedirected || Console.IsOutputRedirected)
        {
            interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture,
                TerminalCommandStrings.ReplicaRequired, canonicalName, replicas.Length));
            return (canonicalName, null);
        }
 
        var picked = await interactionService.PromptForSelectionAsync(
            string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.SelectReplica, canonicalName),
            replicas,
            r => r.IsAlive
                ? string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ReplicaRunning, r.Label)
                : string.Format(CultureInfo.CurrentCulture, TerminalCommandStrings.ReplicaExited,
                    r.Label, r.ExitCode?.ToString(CultureInfo.InvariantCulture) ?? "unknown"),
            cancellationToken: cancellationToken).ConfigureAwait(false);
 
        return (canonicalName, picked);
    }
}