File: Agents\Copilot\CopilotCliRunner.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.Diagnostics;
using Microsoft.Extensions.Logging;
using Semver;
 
namespace Aspire.Cli.Agents.Copilot;
 
/// <summary>
/// Runs GitHub Copilot CLI commands.
/// </summary>
/// <param name="logger">The logger for diagnostic output.</param>
internal sealed class CopilotCliRunner(ILogger<CopilotCliRunner> logger) : ICopilotCliRunner
{
    /// <inheritdoc />
    public async Task<SemVersion?> GetVersionAsync(CancellationToken cancellationToken)
    {
        logger.LogDebug("Checking for GitHub Copilot CLI installation");
 
        var executablePath = PathLookupHelper.FindFullPathFromPath("copilot");
        if (executablePath is null)
        {
            logger.LogDebug("GitHub Copilot CLI is not installed or not found in PATH");
            return null;
        }
 
        try
        {
            var result = await Process.RunAndCaptureTextAsync(new ProcessStartInfo(executablePath, "--version")
            {
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            }, cancellationToken).ConfigureAwait(false);
 
            if (result.ExitStatus.ExitCode != 0)
            {
                logger.LogDebug("GitHub Copilot CLI returned non-zero exit code {ExitCode}: {Error}", result.ExitStatus.ExitCode, result.StandardError.Trim());
                return null;
            }
 
            var output = result.StandardOutput;
 
            if (TryParseVersionOutput(output, out var version))
            {
                logger.LogDebug("Found GitHub Copilot CLI version: {Version}", version);
                return version;
            }
 
            logger.LogDebug("Could not parse GitHub Copilot CLI version from output: {Output}", output.Trim());
            return null;
        }
        catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception)
        {
            logger.LogDebug(ex, "GitHub Copilot CLI is not installed or not found in PATH");
            return null;
        }
    }
 
    internal static bool TryParseVersionOutput(string output, out SemVersion? version)
    {
        version = null;
        var versionString = output.Trim();
 
        if (string.IsNullOrEmpty(versionString))
        {
            return false;
        }
 
        // Version output may be on the first line if multi-line
        var lines = versionString.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
        if (lines.Length > 0)
        {
            versionString = lines[0].Trim();
        }
 
        // Try to extract the version from known formats like "GitHub Copilot CLI 0.0.397"
        var lastSpaceIndex = versionString.LastIndexOf(' ');
        if (lastSpaceIndex >= 0)
        {
            versionString = versionString[(lastSpaceIndex + 1)..];
        }
 
        // Trim common trailing punctuation that may follow the version (for example, "0.0.397.")
        versionString = versionString.TrimEnd('.', ',', ')');
 
        // Try to parse the version string (may have a 'v' prefix like "v1.2.3")
        if (versionString.StartsWith('v') || versionString.StartsWith('V'))
        {
            versionString = versionString[1..];
        }
 
        return SemVersion.TryParse(versionString, SemVersionStyles.Any, out version);
    }
}