|
| 1 | +using System; |
| 2 | +using System.Diagnostics; |
| 3 | +using System.IO; |
| 4 | +using System.Linq; |
| 5 | +using System.Runtime.InteropServices; |
| 6 | +using UnityEditor; |
| 7 | + |
| 8 | +namespace UnityMcpBridge.Editor.Helpers |
| 9 | +{ |
| 10 | + internal static class ExecPath |
| 11 | + { |
| 12 | + private const string PrefClaude = "UnityMCP.ClaudeCliPath"; |
| 13 | + |
| 14 | + // Resolve Claude CLI absolute path. Pref → env → common locations → PATH. |
| 15 | + internal static string ResolveClaude() |
| 16 | + { |
| 17 | + try |
| 18 | + { |
| 19 | + string pref = EditorPrefs.GetString(PrefClaude, string.Empty); |
| 20 | + if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref; |
| 21 | + } |
| 22 | + catch { } |
| 23 | + |
| 24 | + string env = Environment.GetEnvironmentVariable("CLAUDE_CLI"); |
| 25 | + if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env; |
| 26 | + |
| 27 | + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
| 28 | + { |
| 29 | + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; |
| 30 | + string[] candidates = |
| 31 | + { |
| 32 | + "/opt/homebrew/bin/claude", |
| 33 | + "/usr/local/bin/claude", |
| 34 | + Path.Combine(home, ".local", "bin", "claude"), |
| 35 | + }; |
| 36 | + foreach (string c in candidates) { if (File.Exists(c)) return c; } |
| 37 | +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX |
| 38 | + return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); |
| 39 | +#else |
| 40 | + return null; |
| 41 | +#endif |
| 42 | + } |
| 43 | + |
| 44 | + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
| 45 | + { |
| 46 | +#if UNITY_EDITOR_WINDOWS |
| 47 | + // Common npm global locations |
| 48 | + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; |
| 49 | + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; |
| 50 | + string[] candidates = |
| 51 | + { |
| 52 | + Path.Combine(appData, "npm", "claude.cmd"), |
| 53 | + Path.Combine(localAppData, "npm", "claude.cmd"), |
| 54 | + }; |
| 55 | + foreach (string c in candidates) { if (File.Exists(c)) return c; } |
| 56 | + string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude"); |
| 57 | + if (!string.IsNullOrEmpty(fromWhere)) return fromWhere; |
| 58 | +#endif |
| 59 | + return null; |
| 60 | + } |
| 61 | + |
| 62 | + // Linux |
| 63 | + { |
| 64 | + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; |
| 65 | + string[] candidates = |
| 66 | + { |
| 67 | + "/usr/local/bin/claude", |
| 68 | + "/usr/bin/claude", |
| 69 | + Path.Combine(home, ".local", "bin", "claude"), |
| 70 | + }; |
| 71 | + foreach (string c in candidates) { if (File.Exists(c)) return c; } |
| 72 | +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX |
| 73 | + return Which("claude", "/usr/local/bin:/usr/bin:/bin"); |
| 74 | +#else |
| 75 | + return null; |
| 76 | +#endif |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // Use existing UV resolver; returns absolute path or null. |
| 81 | + internal static string ResolveUv() |
| 82 | + { |
| 83 | + return ServerInstaller.FindUvPath(); |
| 84 | + } |
| 85 | + |
| 86 | + internal static bool TryRun( |
| 87 | + string file, |
| 88 | + string args, |
| 89 | + string workingDir, |
| 90 | + out string stdout, |
| 91 | + out string stderr, |
| 92 | + int timeoutMs = 15000, |
| 93 | + string extraPathPrepend = null) |
| 94 | + { |
| 95 | + stdout = string.Empty; |
| 96 | + stderr = string.Empty; |
| 97 | + try |
| 98 | + { |
| 99 | + var psi = new ProcessStartInfo |
| 100 | + { |
| 101 | + FileName = file, |
| 102 | + Arguments = args, |
| 103 | + WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir, |
| 104 | + UseShellExecute = false, |
| 105 | + RedirectStandardOutput = true, |
| 106 | + RedirectStandardError = true, |
| 107 | + CreateNoWindow = true, |
| 108 | + }; |
| 109 | + if (!string.IsNullOrEmpty(extraPathPrepend)) |
| 110 | + { |
| 111 | + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; |
| 112 | + psi.Environment["PATH"] = string.IsNullOrEmpty(currentPath) |
| 113 | + ? extraPathPrepend |
| 114 | + : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath); |
| 115 | + } |
| 116 | + using var p = Process.Start(psi); |
| 117 | + if (p == null) return false; |
| 118 | + stdout = p.StandardOutput.ReadToEnd(); |
| 119 | + stderr = p.StandardError.ReadToEnd(); |
| 120 | + if (!p.WaitForExit(timeoutMs)) { try { p.Kill(); } catch { } return false; } |
| 121 | + return p.ExitCode == 0; |
| 122 | + } |
| 123 | + catch |
| 124 | + { |
| 125 | + return false; |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX |
| 130 | + private static string Which(string exe, string prependPath) |
| 131 | + { |
| 132 | + try |
| 133 | + { |
| 134 | + var psi = new ProcessStartInfo("/usr/bin/which", exe) |
| 135 | + { |
| 136 | + UseShellExecute = false, |
| 137 | + RedirectStandardOutput = true, |
| 138 | + CreateNoWindow = true, |
| 139 | + }; |
| 140 | + string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; |
| 141 | + psi.Environment["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path); |
| 142 | + using var p = Process.Start(psi); |
| 143 | + string output = p?.StandardOutput.ReadToEnd().Trim(); |
| 144 | + p?.WaitForExit(1500); |
| 145 | + return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null; |
| 146 | + } |
| 147 | + catch { return null; } |
| 148 | + } |
| 149 | +#endif |
| 150 | + |
| 151 | +#if UNITY_EDITOR_WINDOWS |
| 152 | + private static string Where(string exe) |
| 153 | + { |
| 154 | + try |
| 155 | + { |
| 156 | + var psi = new ProcessStartInfo("where", exe) |
| 157 | + { |
| 158 | + UseShellExecute = false, |
| 159 | + RedirectStandardOutput = true, |
| 160 | + CreateNoWindow = true, |
| 161 | + }; |
| 162 | + using var p = Process.Start(psi); |
| 163 | + string first = p?.StandardOutput.ReadToEnd() |
| 164 | + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) |
| 165 | + .FirstOrDefault(); |
| 166 | + p?.WaitForExit(1500); |
| 167 | + return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null; |
| 168 | + } |
| 169 | + catch { return null; } |
| 170 | + } |
| 171 | +#endif |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | + |
0 commit comments