fix: make copilot CLI path resolution cross-platform

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 23:44:49 +01:00
co-authored by Copilot
parent f94c3232b4
commit d88d00df0d
2 changed files with 62 additions and 6 deletions
@@ -8,6 +8,10 @@ internal static class CopilotCliPathResolver
private const string CopilotCommandName = "copilot";
private const string DefaultWindowsCommandProcessor = "cmd.exe";
private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD";
private const char WindowsSearchPathSeparator = ';';
private const char UnixSearchPathSeparator = ':';
private const char WindowsDirectorySeparator = '\\';
private const char UnixDirectorySeparator = '/';
private static readonly string[] BlockedCliEnvironmentPrefixes = ["BUN_", "COPILOT_", "ELECTRON_", "NODE_", "NPM_"];
@@ -125,7 +129,7 @@ internal static class CopilotCliPathResolver
{
foreach (string candidateName in GetCandidateFileNames(pathExtValue, isWindows))
{
string candidatePath = Path.Combine(directory, candidateName);
string candidatePath = CombineSearchPath(directory, candidateName, isWindows);
if (fileExists(candidatePath))
{
return candidatePath;
@@ -145,7 +149,7 @@ internal static class CopilotCliPathResolver
StringComparer comparer = isWindows ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
foreach (string directory in pathValue
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Split(GetSearchPathSeparator(isWindows), StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(segment => segment.Trim('"'))
.Where(segment => !string.IsNullOrWhiteSpace(segment))
.Distinct(comparer))
@@ -184,6 +188,38 @@ internal static class CopilotCliPathResolver
}
}
private static char GetSearchPathSeparator(bool isWindows)
{
return isWindows ? WindowsSearchPathSeparator : UnixSearchPathSeparator;
}
private static string CombineSearchPath(string directory, string fileName, bool isWindows)
{
if (string.IsNullOrEmpty(directory))
{
return fileName;
}
if (EndsWithDirectorySeparator(directory, isWindows))
{
return directory + fileName;
}
return directory + GetDirectorySeparator(isWindows) + fileName;
}
private static bool EndsWithDirectorySeparator(string path, bool isWindows)
{
char lastCharacter = path[^1];
return lastCharacter == GetDirectorySeparator(isWindows)
|| (isWindows && lastCharacter == UnixDirectorySeparator);
}
private static char GetDirectorySeparator(bool isWindows)
{
return isWindows ? WindowsDirectorySeparator : UnixDirectorySeparator;
}
private static IEnumerable<KeyValuePair<string, string?>> GetCurrentEnvironmentVariables()
{
return Environment.GetEnvironmentVariables()
@@ -8,9 +8,10 @@ public sealed class CopilotCliPathResolverTests
public void Resolve_UsesCopilotFromPath()
{
string copilotDirectory = @"C:\tools\copilot";
string expectedCliPath = @"C:\tools\copilot\copilot.exe";
HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase)
{
Path.Combine(copilotDirectory, "copilot.exe"),
expectedCliPath,
};
string? cliPath = CopilotCliPathResolver.Resolve(
@@ -19,16 +20,17 @@ public sealed class CopilotCliPathResolverTests
isWindows: true,
fileExists: existingFiles.Contains);
Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), cliPath, ignoreCase: true);
Assert.Equal(expectedCliPath, cliPath, ignoreCase: true);
}
[Fact]
public void Resolve_UsesDefaultWindowsExtensionsWhenPathExtMissing()
{
string copilotDirectory = @"C:\tools\copilot";
string expectedCliPath = @"C:\tools\copilot\copilot.cmd";
HashSet<string> existingFiles = new(StringComparer.OrdinalIgnoreCase)
{
Path.Combine(copilotDirectory, "copilot.cmd"),
expectedCliPath,
};
string? cliPath = CopilotCliPathResolver.Resolve(
@@ -37,7 +39,25 @@ public sealed class CopilotCliPathResolverTests
isWindows: true,
fileExists: existingFiles.Contains);
Assert.Equal(Path.Combine(copilotDirectory, "copilot.cmd"), cliPath, ignoreCase: true);
Assert.Equal(expectedCliPath, cliPath, ignoreCase: true);
}
[Fact]
public void Resolve_UsesCopilotFromPathOutsideWindows()
{
const string expectedCliPath = "/usr/local/bin/copilot";
HashSet<string> existingFiles = new(StringComparer.Ordinal)
{
expectedCliPath,
};
string? cliPath = CopilotCliPathResolver.Resolve(
pathValue: "/usr/bin:/usr/local/bin",
pathExtValue: null,
isWindows: false,
fileExists: existingFiles.Contains);
Assert.Equal(expectedCliPath, cliPath);
}
[Fact]