diff --git a/.code-linter.json b/.code-linter.json index 2d325db..a16cfd4 100644 --- a/.code-linter.json +++ b/.code-linter.json @@ -1,10 +1,10 @@ { - "max_file_lines": 750, - "max_function_lines": 150, - "max_nesting_depth": 8, - "max_parameters": 8, - "max_comment_lines": 15, - "max_types_per_file": 6, + "max_file_lines": 300, + "max_function_lines": 50, + "max_nesting_depth": 4, + "max_parameters": 5, + "max_comment_lines": 5, + "max_types_per_file": 2, "include_extensions": [ ".cs", ".py" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b6496c5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +* @daliys + +# Quality Gate & CI Policy files strictly owned by @daliys +/.code-linter.json @daliys +/.ruff.toml @daliys +/.unity-quality-gate.json @daliys +/.swift-quality-gate.json @daliys +/.slop-review.json @daliys +/.github/workflows/ @daliys +/.github/CODEOWNERS @daliys diff --git a/API_REFERENCE.MD b/API_REFERENCE.MD index 2617c4f..dfabc36 100644 --- a/API_REFERENCE.MD +++ b/API_REFERENCE.MD @@ -192,7 +192,7 @@ Actions: `undo`, `redo`, `play`, `pause`, `step`, `menu`, `read_logs`, `clear_lo ### `unity_ui_automation` Actions: `list_windows`, `get_hierarchy`, `query`, `get_window_rect`, `set_window_rect`, `capture_window_snapshot`, `click`, `input`. -`get_hierarchy` accepts `deep`. `query` accepts `name`, `text`, and `class_name`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy, with best-effort PNG image capture on macOS. +`get_hierarchy` accepts `deep`, `max_depth`, and `max_elements` (with `children_truncated` and root `truncated` markers when capped). `query` accepts `name`, `text`, `class_name`, `max_depth`, and `max_results`. Window rect and snapshot actions are intended for resize/layout QA; window snapshots include rect and hierarchy (supporting `max_depth` and `max_elements`), with best-effort PNG image capture on macOS. ### `unity_wait` Conditions: `compilation`, `play_mode`, `import`, `editor_idle`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e37e3c..847121f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable public changes to Nexus Unity are documented here. - Consolidated duplicate internal Ollama-review and serialized-property write helpers. ### Fixed +- Add traversal depth and element/result bounds to UI Toolkit hierarchy serialization and element queries (`ui_get_hierarchy`, `ui_query_elements`, and `ui_capture_window_snapshot`), preventing editor stalls and payload bloat on complex UI windows, with `children_truncated` and `truncated` markers on partial hierarchy responses (#126). - Dispatch WebSocket request handling asynchronously in `Task.Run`, ensuring `ServerLoop` remains non-blocking for concurrent HTTP JSON-RPC requests and new connections (#129). - `EditorApplication.update` subscription for `HandlePostCompileFocusReturn` is now idempotent (unsubscribes before subscribing), preventing stacked duplicate frame handlers on repeated initialization or domain reloads (#138). - `ListPlayerPrefs` now uses `ProcessStartInfo.ArgumentList` and timeout guards for macOS `defaults read` execution, preventing process hangs, alongside regex unhashing for Windows registry keys, Linux XML prefs support, and double-default type verification (#143). diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD index 654f90b..7cab974 100644 --- a/DOCUMENTATION.MD +++ b/DOCUMENTATION.MD @@ -117,7 +117,7 @@ MCP bridge: - `unity_hierarchy_manager` can create primitives with name, parent, transform, and material path; it can also rename, set transforms, and pass through `create_hierarchy`. - Raw `run_tests` returns `Submitted` when Unity accepts the asynchronous request; it does not confirm that execution has begun or completed. `unity_editor_controller` `run_tests_wait` waits in the Python bridge by polling raw `get_test_results` instead of blocking the Unity main thread. - `unity_editor_controller` also exposes `get_tool_usage_stats` and `reset_tool_usage_stats` so diagnostics are not split between raw and manager workflows. -- `unity_ui_automation` passes through `deep` for UI hierarchy reads, `class_name` for queries, and window rect/snapshot actions for layout checks. +- `unity_ui_automation` passes through `deep`, `max_depth`, and `max_elements` for UI hierarchy reads, `class_name`, `max_depth`, and `max_results` for queries, and window rect/snapshot actions for layout checks. ## Current Compatibility Decisions diff --git a/Editor/MCPCliInstaller.ClaudeCode.cs b/Editor/MCPCliInstaller.ClaudeCode.cs index e7734f0..619901c 100644 --- a/Editor/MCPCliInstaller.ClaudeCode.cs +++ b/Editor/MCPCliInstaller.ClaudeCode.cs @@ -1,8 +1,8 @@ -using UnityEditor; -using UnityEngine; -using System.IO; using System; +using System.IO; using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; namespace UnityMCP.Editor { @@ -15,9 +15,7 @@ public static partial class MCPCliInstaller /// /// Prefers the official claude CLI (claude mcp add --scope project, which itself writes /// .mcp.json). When the CLI is unavailable or fails, it falls back to writing .mcp.json directly. - /// Either path produces the same project-root .mcp.json that the Integrations tab tracks. This is distinct - /// from the Claude Desktop app config, which is handled by the generic JSON-config codepath in - /// instead of a dedicated CLI installer. + /// Either path produces the same project-root .mcp.json that the Integrations tab tracks. /// public static void LinkToClaudeCode() { @@ -31,45 +29,43 @@ public static void LinkToClaudeCode() private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pythonPath) { string claudePath = ResolveExecutablePath("claude"); - - // Preferred path: the official claude CLI writes the project-scoped .mcp.json for us. if (!string.IsNullOrEmpty(claudePath) && claudePath != "claude") { - // 1. Remove any stale registration so re-running is idempotent. - // A missing registration is expected on first setup and is safe to add over. - bool removedStaleRegistration = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), claudePath, false, "Claude Code", out string removeError, false); - bool registrationWasAbsent = !removedStaleRegistration && IsClaudeCodeRegistrationAbsent(removeError); - - // 2. Add the server at project scope only when the prior state is known to be clear. - if ((removedStaleRegistration || registrationWasAbsent) && RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "--transport", "stdio", "--scope", "project", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", "--", pythonPath, scriptPath), claudePath, false, "Claude Code")) + if (TryLinkViaClaudeCli(claudePath, scriptPath, pythonPath)) { - NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Successfully linked Nexus Unity to Claude Code via '" + claudePath + "'.", true); - EditorUtility.DisplayDialog("MCP Success", "Successfully linked Nexus Unity to Claude Code.\n\nRun /mcp inside Claude Code (or restart it) to load the server.", "OK"); return; } + } + + WriteClaudeCodeDirectJson(scriptPath, pythonPath); + } - NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStaleRegistration || registrationWasAbsent) - ? "[MCP] Claude Code CLI add command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit." - : "[MCP] Claude Code CLI could not remove the existing registration at '" + claudePath + "': " + removeError + ". Skipping CLI add and falling back to direct .mcp.json edit."); + private static bool TryLinkViaClaudeCli(string claudePath, string scriptPath, string pythonPath) + { + bool removedStale = RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "remove", "--scope", "project", "nexus-unity"), false, "Claude Code", out string removeError, false); + bool registrationAbsent = !removedStale && IsClaudeCodeRegistrationAbsent(removeError); + + if ((removedStale || registrationAbsent) && RunInstallerProcess(CreateProcessStartInfo(claudePath, "mcp", "add", "--transport", "stdio", "--scope", "project", "--env", MCPServer.AuthTokenEnvironmentVariable + "=" + MCPServer.AuthToken, "nexus-unity", "--", pythonPath, scriptPath), claudePath, false, "Claude Code")) + { + NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Successfully linked Nexus Unity to Claude Code via '" + claudePath + "'.", true); + EditorUtility.DisplayDialog("MCP Success", "Successfully linked Nexus Unity to Claude Code.\n\nRun /mcp inside Claude Code (or restart it) to load the server.", "OK"); + return true; } - // Fallback: write the project-root .mcp.json directly. + NexusEditorLog.Warning(NexusLogCategory.Integrations, (removedStale || registrationAbsent) + ? "[MCP] Claude Code CLI add command failed at '" + claudePath + "'. Falling back to direct .mcp.json edit." + : "[MCP] Claude Code CLI could not remove the existing registration at '" + claudePath + "': " + removeError + ". Skipping CLI add and falling back to direct .mcp.json edit."); + return false; + } + + private static void WriteClaudeCodeDirectJson(string scriptPath, string pythonPath) + { try { string projectRoot = Path.GetDirectoryName(Application.dataPath); string configPath = Path.Combine(projectRoot, ".mcp.json"); - JObject config; - if (File.Exists(configPath)) - { - try { config = JObject.Parse(File.ReadAllText(configPath)); } - catch { config = new JObject(); } - } - else - { - config = new JObject(); - } - + JObject config = LoadOrCreateJsonObject(configPath); if (config["mcpServers"] == null) config["mcpServers"] = new JObject(); JObject servers = (JObject)config["mcpServers"]; @@ -93,6 +89,16 @@ private static void ExecuteClaudeCodeLinkSequence(string scriptPath, string pyth } } + private static JObject LoadOrCreateJsonObject(string path) + { + if (File.Exists(path)) + { + try { return JObject.Parse(File.ReadAllText(path)); } + catch { return new JObject(); } + } + return new JObject(); + } + internal static bool IsClaudeCodeRegistrationAbsent(string error) { return !string.IsNullOrEmpty(error) diff --git a/Editor/MCPCliInstaller.Deploy.cs b/Editor/MCPCliInstaller.Deploy.cs new file mode 100644 index 0000000..e3666bc --- /dev/null +++ b/Editor/MCPCliInstaller.Deploy.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace UnityMCP.Editor +{ + public static partial class MCPCliInstaller + { + private static bool DeployBridgeScript(out string destinationPath) + { + destinationPath = null; + string sourcePath = FindBridgeScript(); + + if (string.IsNullOrEmpty(sourcePath)) + { + NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Could not find 'nexus_unity_bridge.py' in the project."); + EditorUtility.DisplayDialog("MCP Error", "Could not find 'nexus_unity_bridge.py'.\n\nEnsure the library is correctly imported.", "OK"); + return false; + } + + string projectRoot = Path.GetDirectoryName(Application.dataPath); + destinationPath = Path.Combine(projectRoot, "nexus_unity_bridge.py"); + + try + { + File.Copy(sourcePath, destinationPath, true); + DeployBridgeModule(projectRoot, sourcePath); + NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge script deployed to stable location: " + destinationPath, true); + DeployDocumentationPointer(projectRoot, sourcePath); + return true; + } + catch (Exception e) + { + NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Failed to deploy bridge or docs: " + e.Message); + EditorUtility.DisplayDialog("MCP Error", "Failed to deploy integration files to project root.\n\n" + e.Message, "OK"); + return false; + } + } + + private static void DeployBridgeModule(string projectRoot, string sourcePath) + { + string sourceDir = Path.GetDirectoryName(sourcePath); + string sourceModuleDir = Path.Combine(sourceDir, "nexus_bridge"); + if (!Directory.Exists(sourceModuleDir)) + { + return; + } + + string destinationModuleDir = Path.Combine(projectRoot, "nexus_bridge"); + CopyDirectory(sourceModuleDir, destinationModuleDir); + NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge module deployed to stable location: " + destinationModuleDir); + } + + private static void CopyDirectory(string sourceDir, string destinationDir) + { + Directory.CreateDirectory(destinationDir); + + foreach (string file in Directory.GetFiles(sourceDir)) + { + string fileName = Path.GetFileName(file); + if (fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) || + fileName.EndsWith(".pyc", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + File.Copy(file, Path.Combine(destinationDir, fileName), true); + } + + foreach (string dir in Directory.GetDirectories(sourceDir)) + { + string dirName = Path.GetFileName(dir); + if (dirName == "__pycache__") + { + continue; + } + + CopyDirectory(dir, Path.Combine(destinationDir, dirName)); + } + } + + private static string FindLibraryRoot(string sourcePath) + { + string dir = Path.GetDirectoryName(sourcePath); + while (!string.IsNullOrEmpty(dir)) + { + if (File.Exists(Path.Combine(dir, "package.json"))) + { + return dir; + } + dir = Path.GetDirectoryName(dir); + } + return null; + } + + private static void DeployDocumentationPointer(string projectRoot, string sourcePath) + { + string libraryRoot = FindLibraryRoot(sourcePath); + if (string.IsNullOrEmpty(libraryRoot)) + { + return; + } + + string docSource = Path.Combine(libraryRoot, "DOCUMENTATION.MD"); + if (!File.Exists(docSource)) + { + return; + } + + string relativeDocPath = GetRelativePath(projectRoot, docSource).Replace("\\", "/"); + string pointerPath = Path.Combine(projectRoot, "NEXUS_UNITY_DOCUMENTATION.md"); + string pointerContent = "# Nexus Unity Documentation\n\n" + + "The canonical Nexus Unity documentation is maintained in the package root:\n\n" + + "- [DOCUMENTATION.MD](" + relativeDocPath + ")\n\n" + + "Keep all edits in the package copy so changes stay with version control.\n"; + + File.WriteAllText(pointerPath, pointerContent); + } + + private static string GetRelativePath(string fromPath, string toPath) + { + Uri fromUri = new Uri(AppendDirectorySeparator(Path.GetFullPath(fromPath))); + Uri toUri = new Uri(Path.GetFullPath(toPath)); + Uri relativeUri = fromUri.MakeRelativeUri(toUri); + return Uri.UnescapeDataString(relativeUri.ToString()); + } + + private static string AppendDirectorySeparator(string path) + { + if (path.EndsWith(Path.DirectorySeparatorChar.ToString()) || + path.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + return path; + } + return path + Path.DirectorySeparatorChar; + } + + private static string FindBridgeScript() + { + string[] guids = AssetDatabase.FindAssets("nexus_unity_bridge"); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + if (path.EndsWith("nexus_unity_bridge.py")) + { + return Path.GetFullPath(path); + } + } + + string manualSearch = Path.Combine(Application.dataPath, "nexus_unity_bridge.py"); + if (File.Exists(manualSearch)) + { + return manualSearch; + } + + foreach (string path in Directory.GetFiles(Application.dataPath, "*.py", SearchOption.AllDirectories)) + { + if (path.EndsWith("nexus_unity_bridge.py")) return Path.GetFullPath(path); + } + return null; + } + } +} diff --git a/Editor/MCPCliInstaller.Deploy.cs.meta b/Editor/MCPCliInstaller.Deploy.cs.meta new file mode 100644 index 0000000..a7c88f2 --- /dev/null +++ b/Editor/MCPCliInstaller.Deploy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be5b21cfbca94822a22c7c66930cd272 diff --git a/Editor/MCPCliInstaller.cs b/Editor/MCPCliInstaller.cs index 47c3e07..24c396a 100644 --- a/Editor/MCPCliInstaller.cs +++ b/Editor/MCPCliInstaller.cs @@ -1,9 +1,9 @@ -using UnityEditor; -using UnityEngine; -using System.IO; -using System.Diagnostics; using System; +using System.Diagnostics; +using System.IO; using System.Text; +using UnityEditor; +using UnityEngine; namespace UnityMCP.Editor { @@ -16,226 +16,77 @@ namespace UnityMCP.Editor /// public static partial class MCPCliInstaller { - - - private static bool DeployBridgeScript(out string destinationPath) - { - destinationPath = null; - string sourcePath = FindBridgeScript(); - - if (string.IsNullOrEmpty(sourcePath)) - { - NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Could not find 'nexus_unity_bridge.py' in the project."); - EditorUtility.DisplayDialog("MCP Error", "Could not find 'nexus_unity_bridge.py'.\n\nEnsure the library is correctly imported.", "OK"); - return false; - } - - string projectRoot = Path.GetDirectoryName(Application.dataPath); - destinationPath = Path.Combine(projectRoot, "nexus_unity_bridge.py"); - - try - { - File.Copy(sourcePath, destinationPath, true); - DeployBridgeModule(projectRoot, sourcePath); - NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge script deployed to stable location: " + destinationPath, true); - DeployDocumentationPointer(projectRoot, sourcePath); - return true; - } - catch (Exception e) - { - NexusEditorLog.Error(NexusLogCategory.Integrations, "[MCP] Failed to deploy bridge or docs: " + e.Message); - EditorUtility.DisplayDialog("MCP Error", "Failed to deploy integration files to project root.\n\n" + e.Message, "OK"); - return false; - } - } - - private static void DeployBridgeModule(string projectRoot, string sourcePath) - { - string sourceDir = Path.GetDirectoryName(sourcePath); - string sourceModuleDir = Path.Combine(sourceDir, "nexus_bridge"); - if (!Directory.Exists(sourceModuleDir)) - { - return; - } - - string destinationModuleDir = Path.Combine(projectRoot, "nexus_bridge"); - CopyDirectory(sourceModuleDir, destinationModuleDir); - NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Bridge module deployed to stable location: " + destinationModuleDir); - } - - private static void CopyDirectory(string sourceDir, string destinationDir) + private static string ResolveExecutablePath(string name) { - Directory.CreateDirectory(destinationDir); - - foreach (string file in Directory.GetFiles(sourceDir)) + if (Application.platform == RuntimePlatform.WindowsEditor) { - string fileName = Path.GetFileName(file); - if (fileName.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) || - fileName.EndsWith(".pyc", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - File.Copy(file, Path.Combine(destinationDir, fileName), true); + string fromWhere = GetPathFromWhere(name); + return string.IsNullOrEmpty(fromWhere) ? name : fromWhere; } - foreach (string dir in Directory.GetDirectories(sourceDir)) + if (name == "codex") { - string dirName = Path.GetFileName(dir); - if (dirName == "__pycache__") - { - continue; - } - - CopyDirectory(dir, Path.Combine(destinationDir, dirName)); + string nvmCodex = ResolveNvmCodex(); + if (!string.IsNullOrEmpty(nvmCodex)) return nvmCodex; } - } - private static string FindLibraryRoot(string sourcePath) - { - string dir = Path.GetDirectoryName(sourcePath); - while (!string.IsNullOrEmpty(dir)) - { - if (File.Exists(Path.Combine(dir, "package.json"))) - { - return dir; - } - dir = Path.GetDirectoryName(dir); - } - return Path.GetDirectoryName(sourcePath); - } - - private static void DeployDocumentationPointer(string projectRoot, string sourcePath) - { - string libraryRoot = FindLibraryRoot(sourcePath); - string[] docFiles = { "API_REFERENCE.MD", "DOCUMENTATION.MD" }; - - foreach (string file in docFiles) + string pathFromWhich = GetPathFromWhich(name); + if (!string.IsNullOrEmpty(pathFromWhich) && pathFromWhich != name && File.Exists(pathFromWhich)) { - string src = Path.Combine(libraryRoot, file); - if (File.Exists(src)) + if (name != "codex" || !pathFromWhich.Contains("homebrew")) { - try - { - string dst = Path.Combine(projectRoot, file); - File.Copy(src, dst, true); - NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Copied documentation to root: " + dst); - } - catch (Exception e) - { - NexusEditorLog.Warning(NexusLogCategory.Integrations, "[MCP] Failed to copy " + file + " to root: " + e.Message); - } + return pathFromWhich; } } - string docPointerPath = Path.Combine(projectRoot, "NEXUS_UNITY_DOCS.md"); - string docContent = "# Nexus Unity - AI Context\n\n" + - "This project uses **Nexus Unity** for AI Editor automation.\n\n" + - "## 📚 Documentation Access\n" + - "- **Full Tool Reference**: [API_REFERENCE.MD](API_REFERENCE.MD)\n" + - "- **Technical Guide**: [DOCUMENTATION.MD](DOCUMENTATION.MD)\n\n" + - "## 🤖 AI Instructions\n" + - "Before performing any Unity tasks, ALWAYS read `API_REFERENCE.MD` to understand the available tools, their parameters, and the surgical editing patterns required for this project."; - - File.WriteAllText(docPointerPath, docContent); - NexusEditorLog.Log(NexusLogCategory.Integrations, "[MCP] Documentation pointer deployed: " + docPointerPath); + return ResolveSystemExecutable(name); } - - - private static string FindBridgeScript() + private static string ResolveNvmCodex() { - string[] guids = AssetDatabase.FindAssets("MCPCliInstaller t:Script"); - foreach (var guid in guids) - { - string path = AssetDatabase.GUIDToAssetPath(guid); - if (!path.Contains("NexusUnity")) continue; - - string dir = Path.GetDirectoryName(path); - string potentialBridge = Path.Combine(dir, "nexus_unity_bridge.py"); - if (File.Exists(Path.GetFullPath(potentialBridge))) return Path.GetFullPath(potentialBridge); - } + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string nvmBase = Path.Combine(home, ".nvm/versions/node"); + if (!Directory.Exists(nvmBase)) return null; - foreach (var path in AssetDatabase.GetAllAssetPaths()) + foreach (var versionDir in Directory.GetDirectories(nvmBase)) { - if (path.EndsWith("nexus_unity_bridge.py")) return Path.GetFullPath(path); + string potential = Path.Combine(versionDir, "bin/codex"); + if (File.Exists(potential)) return potential; } return null; } - private static string ResolveExecutablePath(string name) + private static string ResolveSystemExecutable(string name) { - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // 'which' does not exist on Windows; use 'where' to resolve against PATH (and PATHEXT, so - // .cmd/.exe shims like gemini.cmd are found). Falls back to the bare name when unresolved. - string fromWhere = GetPathFromWhere(name); - return string.IsNullOrEmpty(fromWhere) ? name : fromWhere; - } - - // 1. Explicit check for NVM/Node paths (High priority for Codex) - if (name == "codex") - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - string nvmBase = Path.Combine(home, ".nvm/versions/node"); - if (Directory.Exists(nvmBase)) - { - foreach (var versionDir in Directory.GetDirectories(nvmBase)) - { - string potential = Path.Combine(versionDir, "bin/codex"); - if (File.Exists(potential)) return potential; - } - } - } - - // 2. Try 'which' (respects shell PATH) - string pathFromWhich = GetPathFromWhich(name); - if (!string.IsNullOrEmpty(pathFromWhich) && pathFromWhich != name && File.Exists(pathFromWhich)) - { - // Only return if it's not the older homebrew version of codex (if we can tell) - if (name == "codex" && pathFromWhich.Contains("homebrew")) { - // Continue to fallback if we prefer NVM - } else { - return pathFromWhich; - } - } - - // 3. Common system paths - string[] searchPaths = { - "/usr/local/bin/" + name, - "/opt/homebrew/bin/" + name, - "/usr/bin/" + name, - "/bin/" + name + string[] searchPaths = { + "/usr/local/bin/" + name, + "/opt/homebrew/bin/" + name, + "/usr/bin/" + name, + "/bin/" + name }; foreach (string path in searchPaths) { if (File.Exists(path)) return path; } - return name; } private static string GetPathFromWhich(string name) { - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = "/usr/bin/which", - Arguments = name, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = name, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true }; if (Application.platform != RuntimePlatform.WindowsEditor) { string pathEnv = ""; try { pathEnv = Environment.GetEnvironmentVariable("PATH"); } catch(Exception) {} - // Unity launched from Finder/Hub (not a terminal) inherits a stripped PATH - // (typically just /usr/bin:/bin:/usr/sbin:/sbin) with no /usr/local/bin or /opt/homebrew/bin. - // 'which' would then resolve python3 to the old Xcode-bundled interpreter at /usr/bin/python3 - // instead of a modern one. Prepend common interpreter locations so they're checked first, - // matching the priority order ResolveExecutablePath's own fallback search list already uses. psi.EnvironmentVariables["PATH"] = "/usr/local/bin:/opt/homebrew/bin:" + pathEnv; } @@ -269,14 +120,13 @@ private static string GetPathFromWhere(string name) { string output = p.StandardOutput.ReadToEnd(); p.WaitForExit(); - if (p.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + if (p.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) return null; + + string[] lines = output.Split('\n'); + for (int i = 0; i < lines.Length; i++) { - // 'where' can list multiple matches, one per line. Take the first that exists on disk. - foreach (string line in output.Split('\n')) - { - string candidate = line.Trim(); - if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) return candidate; - } + string candidate = lines[i].Trim(); + if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) return candidate; } } } @@ -284,14 +134,6 @@ private static string GetPathFromWhere(string name) return null; } - /// - /// Resolves the Python interpreter to embed in generated MCP configs, preferring a concrete path. - /// Tries python3, then python, then the Windows py launcher (which defaults to Python 3). - /// - /// - /// Generated configs previously hardcoded python3, which is frequently absent from PATH on Windows - /// (where the interpreter is usually python or the py launcher), so the bridge failed to launch. - /// private static string ResolvePythonPath() { string python3 = ResolveExecutablePath("python3"); @@ -300,7 +142,6 @@ private static string ResolvePythonPath() string python = ResolveExecutablePath("python"); if (!string.IsNullOrEmpty(python) && python != "python") return python; - // Windows commonly ships the 'py' launcher instead of a 'python3' alias; 'py