Skip to content

Commit 86198a0

Browse files
authored
Merge pull request CoplayDev#206 from dsarno/feat/local-resolution-and-claude-cli
Feat: Local-only package resolution + Claude CLI resolver; quieter installer logs
2 parents 642210c + bd6114b commit 86198a0

File tree

7 files changed

+504
-362
lines changed

7 files changed

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

UnityMcpBridge/Editor/Helpers/ExecPath.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)