Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions MCPForUnity/Editor/Dependencies/UvInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Runs the official uv installer for the current platform. UI-agnostic: build the command,
/// run it (off the main thread), and report the outcome. The caller owns confirmation and
/// re-checking dependencies afterwards.
/// Installer reference: https://docs.astral.sh/uv/getting-started/installation/
/// </summary>
public static class UvInstaller
{
public readonly struct UvInstallResult
{
public readonly bool Success;
public readonly string Output;

public UvInstallResult(bool success, string output)
{
Success = success;
Output = output ?? string.Empty;
}
}

/// <summary>One-click install is available on Windows, macOS and Linux.</summary>
public static bool IsSupported =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

/// <summary>
/// Build the installer invocation for the current platform. Pure and testable — the
/// returned command is exactly what <see cref="Run"/> executes and what the confirm
/// dialog shows the user.
/// </summary>
public static (string file, string arguments) BuildInstallCommand()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ("powershell",
"-NoProfile -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"");
}

// macOS and Linux share the POSIX shell installer.
return ("/bin/sh", "-c \"curl -LsSf https://astral.sh/uv/install.sh | sh\"");
}

/// <summary>Human-readable one-line description of the command that will run.</summary>
public static string DescribeCommand()
{
var (file, arguments) = BuildInstallCommand();
return $"{file} {arguments}";
}

/// <summary>
/// Run the installer and return the outcome. Blocks until the installer exits or the
/// timeout elapses, so call it off the main thread (e.g. via Task.Run).
/// </summary>
public static UvInstallResult Run(int timeoutMs = 180000)
{
try
{
var (file, arguments) = BuildInstallCommand();
bool ok = ExecPath.TryRun(file, arguments, null, out string stdout, out string stderr, timeoutMs);
return new UvInstallResult(ok, Combine(stdout, stderr));
}
catch (Exception ex)
{
return new UvInstallResult(false, ex.Message);
}
}

private static string Combine(string stdout, string stderr)
{
string outText = (stdout ?? string.Empty).Trim();
string errText = (stderr ?? string.Empty).Trim();
string combined =
string.IsNullOrEmpty(errText) ? outText :
string.IsNullOrEmpty(outText) ? errText :
outText + "\n" + errText;

// Keep dialogs readable — echo only the tail of long installer output.
const int max = 1500;
if (combined.Length > max)
{
combined = "…" + combined.Substring(combined.Length - max);
}
return combined;
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Dependencies/UvInstaller.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions MCPForUnity/Editor/Helpers/ExecPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal static string ResolveClaude()
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
Expand All @@ -59,6 +60,7 @@ internal static string ResolveClaude()
Path.Combine(localAppData, "Programs", "claude", "claude.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
Path.Combine(home, ".local", "bin", "claude.exe"),
Path.Combine(home, ".claude", "local", "claude.exe"), // `claude migrate-installer` location
// npm global locations (.cmd preferred for non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
Expand All @@ -81,6 +83,7 @@ internal static string ResolveClaude()
"/usr/local/bin/claude",
"/usr/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
Expand Down
58 changes: 4 additions & 54 deletions MCPForUnity/Editor/Services/PathResolverService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,60 +107,10 @@ public string GetClaudeCliPath()
return null;
}

// No override - use platform-specific discovery
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string[] candidates = new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude.exe"),
};

foreach (var c in candidates)
{
if (File.Exists(c)) return c;
}

#if UNITY_EDITOR_WIN
// Fall back to PATH search (handles non-standard install locations and npm shims)
foreach (var name in new[] { "claude.exe", "claude.cmd", "claude.ps1" })
{
string fromPath = ExecPath.FindInPathWindows(name);
if (!string.IsNullOrEmpty(fromPath)) return fromPath;
}
#endif
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
string[] candidates = new[]
{
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
};

foreach (var c in candidates)
{
if (File.Exists(c)) return c;
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string[] candidates = new[]
{
"/usr/bin/claude",
"/usr/local/bin/claude",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
};

foreach (var c in candidates)
{
if (File.Exists(c)) return c;
}
}

return null;
// No override: delegate to the shared discovery in ExecPath, which covers the
// native-installer, npm, NVM and PATH-scan locations for every platform. Kept in
// one place so both call sites (this and ExecPath's own callers) stay in sync.
return ExecPath.ResolveClaude();
}

public bool IsPythonDetected()
Expand Down
8 changes: 8 additions & 0 deletions MCPForUnity/Editor/Windows/Components/Branding.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

163 changes: 163 additions & 0 deletions MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using UnityEngine;
using UnityEngine.UIElements;

namespace MCPForUnity.Editor.Windows.Components.Branding
{
/// <summary>
/// The MCP for Unity "Ocean split-cube" brand mark.
///
/// On Unity 2022.1+ it is drawn as resolution-independent vector geometry via UI Toolkit's
/// Painter2D — crisp at any DPI, theme-independent (fixed brand colors matching the source
/// SVG), and with no package dependency. Painter2D does not exist on the 2021.3 floor, so
/// there we fall back to the raster brand icon (package-icon.png) that ships with the package.
///
/// Geometry is transcribed from website/static/img/logo-mark.svg (viewBox "30 30 140 140"),
/// which remains the human-facing source of truth. Size the element via USS (width/height);
/// the drawing scales to the resolved content box.
/// </summary>
public sealed class OceanMark : VisualElement
{
public OceanMark()
{
#if UNITY_2022_1_OR_NEWER
generateVisualContent += OnGenerateVisualContent;
RegisterCallback<GeometryChangedEvent>(_ => MarkDirtyRepaint());
#else
ApplyRasterFallback();
#endif
pickingMode = PickingMode.Ignore; // purely decorative
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#if UNITY_2022_1_OR_NEWER
// The source SVG viewBox spans 30..170 on both axes (a 140-unit square).
private const float SvgOrigin = 30f;
private const float SvgSize = 140f;

// Brand palette (hex values lifted directly from logo-mark.svg).
private static readonly Color LeftFill = FromHex(0x2563EB); // blue cube faces
private static readonly Color LeftStroke = FromHex(0x60A5FA); // blue cube outline
private static readonly Color RightFill = FromHex(0x0D9488); // teal cube faces
private static readonly Color RightStroke = FromHex(0x2DD4BF);// teal cube outline
private static readonly Color Bridge = FromHex(0x22D3EE); // cyan bridge

private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
Rect rect = contentRect;
float box = Mathf.Min(rect.width, rect.height);
if (box <= 1f) return; // not laid out yet / too small to draw

float scale = box / SvgSize;
Vector2 P(float x, float y) => MapSvgPoint(x, y, rect);
float S(float w) => w * scale;

Painter2D p = mgc.painter2D;
p.lineJoin = LineJoin.Round;
p.lineCap = LineCap.Round;

// --- Left cube (blue) ---
FillPoly(p, WithAlpha(LeftFill, 0.22f), P(94, 40), P(42, 70), P(94, 100));
FillPoly(p, WithAlpha(LeftFill, 0.12f), P(42, 70), P(42, 130), P(94, 160), P(94, 100));
StrokePath(p, LeftStroke, S(3f), true, P(94, 40), P(42, 70), P(42, 130), P(94, 160));
StrokePath(p, WithAlpha(LeftStroke, 0.65f), S(1.8f), false, P(42, 70), P(94, 100));

// --- Right cube (teal) ---
FillPoly(p, WithAlpha(RightFill, 0.22f), P(106, 40), P(158, 70), P(106, 100));
FillPoly(p, WithAlpha(RightFill, 0.12f), P(158, 70), P(158, 130), P(106, 160), P(106, 100));
StrokePath(p, RightStroke, S(3f), true, P(106, 40), P(158, 70), P(158, 130), P(106, 160));
StrokePath(p, WithAlpha(RightStroke, 0.65f), S(1.8f), false, P(158, 70), P(106, 100));

// --- Bridge rungs (cyan) ---
StrokePath(p, Bridge, S(3f), false, P(94, 70), P(106, 70));
StrokePath(p, Bridge, S(3f), false, P(94, 100), P(106, 100));
StrokePath(p, Bridge, S(3f), false, P(94, 130), P(106, 130));

// --- Bridge dots (cyan) ---
float dot = S(2.2f);
FillDot(p, Bridge, P(94, 70), dot);
FillDot(p, Bridge, P(106, 70), dot);
FillDot(p, Bridge, P(94, 100), dot);
FillDot(p, Bridge, P(106, 100), dot);
FillDot(p, Bridge, P(94, 130), dot);
FillDot(p, Bridge, P(106, 130), dot);
}

private static void FillPoly(Painter2D p, Color color, params Vector2[] pts)
{
p.fillColor = color;
p.BeginPath();
p.MoveTo(pts[0]);
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
p.ClosePath();
p.Fill();
}

private static void StrokePath(Painter2D p, Color color, float width, bool closed, params Vector2[] pts)
{
p.strokeColor = color;
p.lineWidth = width;
p.BeginPath();
p.MoveTo(pts[0]);
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
if (closed) p.ClosePath();
p.Stroke();
}

private static void FillDot(Painter2D p, Color color, Vector2 center, float radius)
{
if (radius <= 0.01f) return;
p.fillColor = color;
p.BeginPath();
p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
p.Fill();
}

private static Color WithAlpha(Color c, float a)
{
c.a = a;
return c;
}

/// <summary>Map a point from SVG viewBox space into the element's content rect (square, centered).</summary>
private static Vector2 MapSvgPoint(float svgX, float svgY, Rect content)
{
float box = Mathf.Min(content.width, content.height);
float scale = box / SvgSize;
float offsetX = (content.width - box) * 0.5f;
float offsetY = (content.height - box) * 0.5f;
return new Vector2(
offsetX + (svgX - SvgOrigin) * scale,
offsetY + (svgY - SvgOrigin) * scale);
}

/// <summary>Convert a 0xRRGGBB literal into an opaque Color.</summary>
private static Color FromHex(int rgb) => new Color(
((rgb >> 16) & 0xFF) / 255f,
((rgb >> 8) & 0xFF) / 255f,
(rgb & 0xFF) / 255f,
1f);
#else
// Painter2D is unavailable before Unity 2022.1 — show the raster brand icon instead.
private void ApplyRasterFallback()
{
Texture2D tex = LoadBrandTexture();
if (tex == null) return;
style.backgroundImage = new StyleBackground(tex);
style.unityBackgroundScaleMode = ScaleMode.ScaleToFit;
}

private static Texture2D LoadBrandTexture()
{
try
{
string root = MCPForUnity.Editor.Helpers.AssetPathUtility.GetMcpPackageRootPath();
return UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>($"{root}/package-icon.png");
}
catch (System.Exception ex)
{
MCPForUnity.Editor.Helpers.McpLog.Warn($"OceanMark: failed to load brand icon fallback: {ex.Message}");
return null;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#endif
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading