mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment The packaged Windows `orca.exe` launcher read `ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the case-sensitive process block into a case-insensitive dictionary via `.Add`. An inherited block carrying both `PATH` and `Path` threw `ArgumentException: Item has already been added. Key in dictionary: 'PATH'`, so every `orca` invocation exited 1 before Electron started (native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67). The launcher now mutates its own environment with `Environment.SetEnvironmentVariable` and never touches either `ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment block and the child inherits the live one verbatim. Orca was also minting the duplicate itself. `applyTerminalAttributionEnv` read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows PTY that inherited `Path` got a second spelling; which one the child resolved was non-deterministic. `createLaunchEnv` did the same and, because its read always missed on Windows, shipped Agent Teams terminals a `PATH` containing only the tmux shim dir. `resolvePathEnvKey` (extracted from the existing precedent in windows-environment-path.ts) now drives every PATH read and write in the PTY env pipeline, and attribution collapses Windows onto the single OS-resolved spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior is unchanged and a case-sensitive POSIX `Path` variable is never touched. Closes #12046 * test(windows): track the launcher's own-environment marker The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the old dictionary writes and failed. Co-authored-by: Orca <help@stably.ai> * fix(windows): follow the host block's PATH spelling on sparse daemon env patches Resolving a path-less Windows env to `Path` handed the daemon's own `{...process.env, ...opts.env}` merge both spellings when the host block spelt `PATH`. Fall back to the host block's own key, and collapse again inside the daemon since that merge happens after attribution. Co-authored-by: Orca <help@stably.ai> * fix(windows): resolve the live PATH spelling by block order, not casing Win32 resolves a duplicated variable by taking the first case-insensitive match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first preference targeted the shadowed spelling on the reporter's own `["PATH","Path"]` block. Drop the attribution-side collapse with it: it deleted the other spelling's value, and deleting the live key promotes the shadowed one, so an env that stripped down to empty lost both. * chore: drop unrelated merge formatting --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
129 lines
4.4 KiB
C#
129 lines
4.4 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
internal static class OrcaCliLauncher
|
|
{
|
|
private static int Main(string[] args)
|
|
{
|
|
try
|
|
{
|
|
string launcherDirectory = Path.GetDirectoryName(typeof(OrcaCliLauncher).Assembly.Location);
|
|
string resourcesDirectory = Directory.GetParent(launcherDirectory).FullName;
|
|
string appDirectory = Directory.GetParent(resourcesDirectory).FullName;
|
|
string electronPath = Path.Combine(appDirectory, "Orca.exe");
|
|
string cliPath = Path.Combine(
|
|
resourcesDirectory,
|
|
"app.asar.unpacked",
|
|
"out",
|
|
"cli",
|
|
"index.js"
|
|
);
|
|
|
|
if (!File.Exists(electronPath))
|
|
{
|
|
Console.Error.WriteLine("Unable to locate Orca.exe next to \"{0}\"", resourcesDirectory);
|
|
return 1;
|
|
}
|
|
|
|
if (!File.Exists(cliPath))
|
|
{
|
|
Console.Error.WriteLine("Unable to locate the Orca CLI entrypoint at \"{0}\"", cliPath);
|
|
return 1;
|
|
}
|
|
|
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = electronPath,
|
|
Arguments = BuildArguments(cliPath, args),
|
|
UseShellExecute = false
|
|
};
|
|
|
|
// Why: launching without cmd.exe preserves embedded newlines while matching the
|
|
// packaged batch launcher's Electron-as-Node environment contract.
|
|
// Why: ProcessStartInfo's env copy rejects duplicate PATH/Path keys; mutating this
|
|
// short-lived process preserves the native block for child inheritance (#12046).
|
|
MoveEnvironmentVariable("NODE_OPTIONS", "ORCA_NODE_OPTIONS");
|
|
MoveEnvironmentVariable("NODE_REPL_EXTERNAL_MODULE", "ORCA_NODE_REPL_EXTERNAL_MODULE");
|
|
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", "1");
|
|
Environment.SetEnvironmentVariable("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1");
|
|
string requestedCliCommand = Environment.GetEnvironmentVariable("ORCA_CLI_COMMAND");
|
|
Environment.SetEnvironmentVariable(
|
|
"ORCA_CLI_COMMAND",
|
|
requestedCliCommand == "orca-ide" ? "orca-ide" : "orca"
|
|
);
|
|
|
|
using (Process child = Process.Start(startInfo))
|
|
{
|
|
child.WaitForExit();
|
|
return child.ExitCode;
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
Console.Error.WriteLine("Unable to start the Orca CLI: {0}", error.Message);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
private static void MoveEnvironmentVariable(string sourceName, string targetName)
|
|
{
|
|
string value = Environment.GetEnvironmentVariable(sourceName);
|
|
Environment.SetEnvironmentVariable(sourceName, null);
|
|
// Why: a null value clears the target, matching the previous unconditional Remove.
|
|
Environment.SetEnvironmentVariable(targetName, value);
|
|
}
|
|
|
|
private static string BuildArguments(string cliPath, string[] args)
|
|
{
|
|
StringBuilder commandLine = new StringBuilder(QuoteArgument(cliPath));
|
|
foreach (string arg in args)
|
|
{
|
|
commandLine.Append(' ');
|
|
commandLine.Append(QuoteArgument(arg));
|
|
}
|
|
return commandLine.ToString();
|
|
}
|
|
|
|
private static string QuoteArgument(string value)
|
|
{
|
|
bool requiresQuotes = value.Length == 0;
|
|
for (int index = 0; index < value.Length && !requiresQuotes; index += 1)
|
|
{
|
|
requiresQuotes = value[index] == '"' || Char.IsWhiteSpace(value[index]);
|
|
}
|
|
if (!requiresQuotes)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
StringBuilder quoted = new StringBuilder("\"");
|
|
int backslashCount = 0;
|
|
foreach (char character in value)
|
|
{
|
|
if (character == '\\')
|
|
{
|
|
backslashCount += 1;
|
|
continue;
|
|
}
|
|
|
|
if (character == '"')
|
|
{
|
|
quoted.Append('\\', backslashCount * 2 + 1);
|
|
quoted.Append('"');
|
|
}
|
|
else
|
|
{
|
|
quoted.Append('\\', backslashCount);
|
|
quoted.Append(character);
|
|
}
|
|
backslashCount = 0;
|
|
}
|
|
|
|
quoted.Append('\\', backslashCount * 2);
|
|
quoted.Append('"');
|
|
return quoted.ToString();
|
|
}
|
|
}
|