mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 08:28:48 +02:00
fix: shell-launch copilot on windows
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
namespace Kopaya.AgentHost.Services;
|
||||
@@ -6,6 +7,7 @@ internal static class CopilotCliPathResolver
|
||||
{
|
||||
private const string CopilotCommandName = "copilot";
|
||||
private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD";
|
||||
private static readonly string[] BlockedCliEnvironmentPrefixes = ["BUN_", "COPILOT_", "ELECTRON_", "NODE_", "NPM_"];
|
||||
|
||||
public static CopilotClientOptions CreateClientOptions()
|
||||
{
|
||||
@@ -21,9 +23,16 @@ internal static class CopilotCliPathResolver
|
||||
"Kopaya requires the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI and ensure it is available in the current environment.");
|
||||
}
|
||||
|
||||
CopilotCliLaunch launch = ResolveCliLaunch(
|
||||
cliPath,
|
||||
OperatingSystem.IsWindows(),
|
||||
Environment.GetEnvironmentVariable("ComSpec"));
|
||||
|
||||
return new CopilotClientOptions
|
||||
{
|
||||
CliPath = cliPath,
|
||||
CliPath = launch.Path,
|
||||
CliArgs = launch.Args,
|
||||
Environment = ResolveCliEnvironment(GetCurrentEnvironmentVariables()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +46,50 @@ internal static class CopilotCliPathResolver
|
||||
return ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists);
|
||||
}
|
||||
|
||||
internal static IReadOnlyDictionary<string, string> ResolveCliEnvironment(
|
||||
IEnumerable<KeyValuePair<string, string?>> environmentVariables)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environmentVariables);
|
||||
|
||||
Dictionary<string, string> sanitizedEnvironment = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (KeyValuePair<string, string?> entry in environmentVariables)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.Key) || entry.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string normalizedKey = entry.Key.ToUpperInvariant();
|
||||
if (BlockedCliEnvironmentPrefixes.Any(prefix => normalizedKey.StartsWith(prefix, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitizedEnvironment[entry.Key] = entry.Value;
|
||||
}
|
||||
|
||||
return sanitizedEnvironment;
|
||||
}
|
||||
|
||||
internal static CopilotCliLaunch ResolveCliLaunch(string cliPath, bool isWindows, string? commandProcessorPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cliPath);
|
||||
|
||||
if (!isWindows)
|
||||
{
|
||||
return new CopilotCliLaunch(cliPath, []);
|
||||
}
|
||||
|
||||
string launchPath = string.IsNullOrWhiteSpace(commandProcessorPath)
|
||||
? "cmd.exe"
|
||||
: commandProcessorPath;
|
||||
|
||||
return new CopilotCliLaunch(
|
||||
launchPath,
|
||||
["/d", "/s", "/c", CopilotCommandName]);
|
||||
}
|
||||
|
||||
private static string? ResolveCliPath(
|
||||
string? pathValue,
|
||||
string? pathExtValue,
|
||||
@@ -98,4 +151,15 @@ internal static class CopilotCliPathResolver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string?>> GetCurrentEnvironmentVariables()
|
||||
{
|
||||
return Environment.GetEnvironmentVariables()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(entry => new KeyValuePair<string, string?>(
|
||||
entry.Key?.ToString() ?? string.Empty,
|
||||
entry.Value?.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record CopilotCliLaunch(string Path, string[] Args);
|
||||
|
||||
@@ -51,4 +51,75 @@ public sealed class CopilotCliPathResolverTests
|
||||
|
||||
Assert.Null(cliPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveCliEnvironment_RemovesRuntimeSpecificPrefixes()
|
||||
{
|
||||
IReadOnlyDictionary<string, string> environment = CopilotCliPathResolver.ResolveCliEnvironment(
|
||||
[
|
||||
new KeyValuePair<string, string?>("PATH", @"C:\tools"),
|
||||
new KeyValuePair<string, string?>("APPDATA", @"C:\Users\mail\AppData\Roaming"),
|
||||
new KeyValuePair<string, string?>("COPILOT_CLI", "1"),
|
||||
new KeyValuePair<string, string?>("NODE_OPTIONS", "--no-warnings"),
|
||||
new KeyValuePair<string, string?>("electron_run_as_node", "1"),
|
||||
new KeyValuePair<string, string?>("BUN_FAKE_FLAG", "1"),
|
||||
new KeyValuePair<string, string?>("npm_config_user_agent", "bun/1.3.6"),
|
||||
]);
|
||||
|
||||
Assert.Equal(
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["PATH"] = @"C:\tools",
|
||||
["APPDATA"] = @"C:\Users\mail\AppData\Roaming",
|
||||
},
|
||||
environment);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveCliEnvironment_PreservesUnrelatedVariables()
|
||||
{
|
||||
IReadOnlyDictionary<string, string> environment = CopilotCliPathResolver.ResolveCliEnvironment(
|
||||
[
|
||||
new KeyValuePair<string, string?>("PATH", @"C:\tools"),
|
||||
new KeyValuePair<string, string?>("HOME", @"C:\Users\mail"),
|
||||
new KeyValuePair<string, string?>("HTTPS_PROXY", "http://proxy.local:8080"),
|
||||
new KeyValuePair<string, string?>("FORCE_COLOR", "1"),
|
||||
]);
|
||||
|
||||
Assert.Equal(
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["PATH"] = @"C:\tools",
|
||||
["HOME"] = @"C:\Users\mail",
|
||||
["HTTPS_PROXY"] = "http://proxy.local:8080",
|
||||
["FORCE_COLOR"] = "1",
|
||||
},
|
||||
environment);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveCliLaunch_UsesCommandProcessorWrapperOnWindows()
|
||||
{
|
||||
CopilotCliLaunch launch = CopilotCliPathResolver.ResolveCliLaunch(
|
||||
cliPath: @"C:\Tools With Spaces\copilot.exe",
|
||||
isWindows: true,
|
||||
commandProcessorPath: @"C:\Windows\System32\cmd.exe");
|
||||
|
||||
Assert.Equal(@"C:\Windows\System32\cmd.exe", launch.Path);
|
||||
Assert.Equal(
|
||||
["/d", "/s", "/c", "copilot"],
|
||||
launch.Args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveCliLaunch_UsesCliDirectlyOutsideWindows()
|
||||
{
|
||||
CopilotCliLaunch launch = CopilotCliPathResolver.ResolveCliLaunch(
|
||||
cliPath: "/usr/local/bin/copilot",
|
||||
isWindows: false,
|
||||
commandProcessorPath: null);
|
||||
|
||||
Assert.Equal("/usr/local/bin/copilot", launch.Path);
|
||||
Assert.Empty(launch.Args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const blockedEnvironmentKeys = new Set(['ELECTRON_RUN_AS_NODE', 'NODE_OPTIONS']);
|
||||
const blockedEnvironmentPrefixes = ['BUN_', 'COPILOT_', 'ELECTRON_', 'NODE_', 'NPM_'];
|
||||
|
||||
export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const sanitizedEnvironment: NodeJS.ProcessEnv = {};
|
||||
@@ -6,7 +6,7 @@ export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): No
|
||||
for (const [name, value] of Object.entries(baseEnvironment)) {
|
||||
const normalizedName = name.toUpperCase();
|
||||
|
||||
if (normalizedName.startsWith('COPILOT_') || blockedEnvironmentKeys.has(normalizedName)) {
|
||||
if (blockedEnvironmentPrefixes.some((prefix) => normalizedName.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,17 @@ describe('createSidecarEnvironment', () => {
|
||||
expect(
|
||||
createSidecarEnvironment({
|
||||
PATH: 'C:\\tools',
|
||||
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
|
||||
COPILOT_CLI: '1',
|
||||
COPILOT_LOADER_PID: '1234',
|
||||
copilot_run_app: '1',
|
||||
bun_install: 'C:\\Users\\mail\\.bun',
|
||||
NODE_OPTIONS: '--no-warnings',
|
||||
electron_run_as_node: '1',
|
||||
npm_config_user_agent: 'bun/1.3.6',
|
||||
}),
|
||||
).toEqual({
|
||||
APPDATA: 'C:\\Users\\mail\\AppData\\Roaming',
|
||||
PATH: 'C:\\tools',
|
||||
});
|
||||
});
|
||||
@@ -24,11 +28,13 @@ describe('createSidecarEnvironment', () => {
|
||||
PATH: 'C:\\tools',
|
||||
HOME: 'C:\\Users\\mail',
|
||||
FORCE_COLOR: '1',
|
||||
HTTPS_PROXY: 'http://proxy.local:8080',
|
||||
}),
|
||||
).toEqual({
|
||||
PATH: 'C:\\tools',
|
||||
HOME: 'C:\\Users\\mail',
|
||||
FORCE_COLOR: '1',
|
||||
HTTPS_PROXY: 'http://proxy.local:8080',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user