mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-04 02:48:44 +02:00
fix: use system copilot in development
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Kopaya.AgentHost.Tests")]
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
namespace Kopaya.AgentHost.Services;
|
||||
|
||||
internal static class CopilotCliPathResolver
|
||||
{
|
||||
private const string CopilotCommandName = "copilot";
|
||||
private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD";
|
||||
|
||||
public static CopilotClientOptions? CreateClientOptions()
|
||||
{
|
||||
CopilotCliResolution resolution = Resolve(
|
||||
Environment.ProcessPath,
|
||||
Environment.GetEnvironmentVariable("PATH"),
|
||||
Environment.GetEnvironmentVariable("PATHEXT"),
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
File.Exists);
|
||||
|
||||
if (!resolution.ShouldOverrideCliPath)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resolution.CliPath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Development sidecar could not find the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI or provide an explicit CliPath.");
|
||||
}
|
||||
|
||||
return new CopilotClientOptions
|
||||
{
|
||||
CliPath = resolution.CliPath,
|
||||
};
|
||||
}
|
||||
|
||||
internal static CopilotCliResolution Resolve(
|
||||
string? processPath,
|
||||
string? pathValue,
|
||||
string? pathExtValue,
|
||||
bool isWindows,
|
||||
Func<string, bool> fileExists)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileExists);
|
||||
|
||||
if (!IsDevelopmentHost(processPath))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return new CopilotCliResolution(
|
||||
ShouldOverrideCliPath: true,
|
||||
CliPath: ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists));
|
||||
}
|
||||
|
||||
private static bool IsDevelopmentHost(string? processPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(processPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
Path.GetFileNameWithoutExtension(processPath),
|
||||
"dotnet",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string? ResolveCliPath(
|
||||
string? pathValue,
|
||||
string? pathExtValue,
|
||||
bool isWindows,
|
||||
Func<string, bool> fileExists)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pathValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringComparer comparer = isWindows ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
|
||||
foreach (string directory in pathValue
|
||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(segment => segment.Trim('"'))
|
||||
.Where(segment => !string.IsNullOrWhiteSpace(segment))
|
||||
.Distinct(comparer))
|
||||
{
|
||||
foreach (string candidateName in GetCandidateFileNames(pathExtValue, isWindows))
|
||||
{
|
||||
string candidatePath = Path.Combine(directory, candidateName);
|
||||
if (fileExists(candidatePath))
|
||||
{
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetCandidateFileNames(string? pathExtValue, bool isWindows)
|
||||
{
|
||||
yield return CopilotCommandName;
|
||||
|
||||
if (!isWindows)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
HashSet<string> yielded = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
CopilotCommandName,
|
||||
};
|
||||
|
||||
string extensions = string.IsNullOrWhiteSpace(pathExtValue)
|
||||
? DefaultWindowsPathExtensions
|
||||
: pathExtValue;
|
||||
|
||||
foreach (string extension in extensions
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(extension => extension.StartsWith('.')))
|
||||
{
|
||||
string candidateName = CopilotCommandName + extension;
|
||||
if (yielded.Add(candidateName))
|
||||
{
|
||||
yield return candidateName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct CopilotCliResolution(bool ShouldOverrideCliPath, string? CliPath);
|
||||
@@ -170,10 +170,11 @@ public sealed class CopilotWorkflowRunner
|
||||
{
|
||||
List<IAsyncDisposable> disposables = [];
|
||||
List<AIAgent> agents = [];
|
||||
CopilotClientOptions? clientOptions = CopilotCliPathResolver.CreateClientOptions();
|
||||
|
||||
foreach (PatternAgentDefinitionDto definition in pattern.Agents)
|
||||
{
|
||||
CopilotClient client = new();
|
||||
CopilotClient client = clientOptions is null ? new() : new(clientOptions);
|
||||
await client.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Kopaya.AgentHost.Services;
|
||||
|
||||
namespace Kopaya.AgentHost.Tests;
|
||||
|
||||
public sealed class CopilotCliPathResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_UsesCopilotFromPathDuringDevelopment()
|
||||
{
|
||||
string copilotDirectory = @"C:\tools\copilot";
|
||||
HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Path.Combine(copilotDirectory, "copilot.exe"),
|
||||
};
|
||||
|
||||
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve(
|
||||
processPath: @"C:\Program Files\dotnet\dotnet.exe",
|
||||
pathValue: $"C:\\other;\"{copilotDirectory}\"",
|
||||
pathExtValue: ".COM;.EXE;.BAT;.CMD",
|
||||
isWindows: true,
|
||||
fileExists: existingFiles.Contains);
|
||||
|
||||
Assert.True(resolution.ShouldOverrideCliPath);
|
||||
Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), resolution.CliPath, ignoreCase: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_LeavesPackagedRuntimeOnBundledCli()
|
||||
{
|
||||
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve(
|
||||
processPath: @"C:\Program Files\Kopaya\Kopaya.AgentHost.exe",
|
||||
pathValue: @"C:\tools",
|
||||
pathExtValue: ".COM;.EXE;.BAT;.CMD",
|
||||
isWindows: true,
|
||||
fileExists: _ => true);
|
||||
|
||||
Assert.False(resolution.ShouldOverrideCliPath);
|
||||
Assert.Null(resolution.CliPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_ReportsMissingCopilotWhenDevelopmentPathDoesNotContainIt()
|
||||
{
|
||||
CopilotCliResolution resolution = CopilotCliPathResolver.Resolve(
|
||||
processPath: @"C:\Program Files\dotnet\dotnet.exe",
|
||||
pathValue: @"C:\tools;C:\other",
|
||||
pathExtValue: ".COM;.EXE;.BAT;.CMD",
|
||||
isWindows: true,
|
||||
fileExists: _ => false);
|
||||
|
||||
Assert.True(resolution.ShouldOverrideCliPath);
|
||||
Assert.Null(resolution.CliPath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user