269 lines
9.1 KiB
C#
269 lines
9.1 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Setup
|
|
{
|
|
public class McpForUnitySkillInstaller : EditorWindow
|
|
{
|
|
private const string RepoUrlKey = "UnityMcpSkillSync.RepoUrl";
|
|
private const string BranchKey = "UnityMcpSkillSync.Branch";
|
|
private const string CliKey = "UnityMcpSkillSync.Cli";
|
|
private const string InstallDirKey = "UnityMcpSkillSync.InstallDir";
|
|
private const string CodexCli = "codex";
|
|
private const string ClaudeCli = "claude";
|
|
private static readonly string[] BranchOptions = { "beta", "main" };
|
|
private static readonly string[] CliOptions = { CodexCli, ClaudeCli };
|
|
|
|
private string _repoUrl;
|
|
private string _targetBranch;
|
|
private string _cliType;
|
|
private string _installDir;
|
|
private Vector2 _scroll;
|
|
private volatile bool _isRunning;
|
|
private readonly ConcurrentQueue<string> _pendingLogs = new();
|
|
private readonly StringBuilder _logBuilder = new(4096);
|
|
|
|
public static void OpenWindow()
|
|
{
|
|
GetWindow<McpForUnitySkillInstaller>("Unity MCP Skill Install(Sync)");
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
_repoUrl = EditorPrefs.GetString(RepoUrlKey, "https://github.com/CoplayDev/unity-mcp");
|
|
_targetBranch = EditorPrefs.GetString(BranchKey, "beta");
|
|
if (!BranchOptions.Contains(_targetBranch))
|
|
{
|
|
_targetBranch = "beta";
|
|
}
|
|
_cliType = EditorPrefs.GetString(CliKey, CodexCli);
|
|
if (!CliOptions.Contains(_cliType))
|
|
{
|
|
_cliType = CodexCli;
|
|
}
|
|
_installDir = EditorPrefs.GetString(InstallDirKey, GetDefaultInstallDir(userHome, _cliType));
|
|
EditorApplication.update += OnEditorUpdate;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EditorApplication.update -= OnEditorUpdate;
|
|
EditorPrefs.SetString(RepoUrlKey, _repoUrl);
|
|
EditorPrefs.SetString(BranchKey, _targetBranch);
|
|
EditorPrefs.SetString(CliKey, _cliType);
|
|
EditorPrefs.SetString(InstallDirKey, _installDir);
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
FlushPendingLogs();
|
|
EditorGUILayout.HelpBox("Sync Unity MCP Skill to the latest on the selected branch and output the changed file list.", MessageType.Info);
|
|
EditorGUILayout.Space(4f);
|
|
|
|
EditorGUILayout.LabelField("Config", EditorStyles.boldLabel);
|
|
using (new EditorGUI.DisabledScope(_isRunning))
|
|
{
|
|
_repoUrl = EditorGUILayout.TextField("Repo URL", _repoUrl);
|
|
var branchIndex = Array.IndexOf(BranchOptions, _targetBranch);
|
|
if (branchIndex < 0)
|
|
{
|
|
branchIndex = 0;
|
|
}
|
|
|
|
var selectedBranchIndex = EditorGUILayout.Popup("Branch", branchIndex, BranchOptions);
|
|
_targetBranch = BranchOptions[selectedBranchIndex];
|
|
|
|
var cliIndex = Array.IndexOf(CliOptions, _cliType);
|
|
if (cliIndex < 0)
|
|
{
|
|
cliIndex = 0;
|
|
}
|
|
|
|
var selectedCliIndex = EditorGUILayout.Popup("CLI", cliIndex, CliOptions);
|
|
if (selectedCliIndex != cliIndex)
|
|
{
|
|
var previousCli = _cliType;
|
|
_cliType = CliOptions[selectedCliIndex];
|
|
TryApplyCliDefaultInstallPath(previousCli, _cliType);
|
|
}
|
|
|
|
_installDir = EditorGUILayout.TextField("Install Dir", _installDir);
|
|
}
|
|
|
|
EditorGUILayout.Space(8f);
|
|
EditorGUILayout.BeginHorizontal();
|
|
using (new EditorGUI.DisabledScope(_isRunning))
|
|
{
|
|
if (GUILayout.Button($"Sync Latest ({_targetBranch})", GUILayout.Height(32f)))
|
|
{
|
|
AppendLineImmediate("Sync task queued...");
|
|
AppendLineImmediate("Will use GitHub API to read the remote directory tree and perform incremental sync (no repository clone).");
|
|
RunSyncLatest();
|
|
}
|
|
}
|
|
|
|
if (GUILayout.Button("Clear Log", GUILayout.Width(100f), GUILayout.Height(32f)))
|
|
{
|
|
_logBuilder.Clear();
|
|
while (_pendingLogs.TryDequeue(out _))
|
|
{
|
|
}
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUILayout.Space(8f);
|
|
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
|
|
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
|
EditorGUILayout.TextArea(_logBuilder.ToString(), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
|
|
private void OnEditorUpdate()
|
|
{
|
|
var changed = FlushPendingLogs();
|
|
if (_isRunning || changed)
|
|
{
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
private void RunSyncLatest()
|
|
{
|
|
if (_isRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isRunning = true;
|
|
SkillSyncService.SyncAsync(_repoUrl, _installDir, _targetBranch,
|
|
line => _pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] {SanitizeLogLine(line)}"),
|
|
result =>
|
|
{
|
|
_isRunning = false;
|
|
if (result.Success)
|
|
{
|
|
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] Sync complete: +{result.Added} ~{result.Updated} -{result.Deleted}");
|
|
}
|
|
else
|
|
{
|
|
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] [ERROR] {result.Error}");
|
|
}
|
|
});
|
|
}
|
|
|
|
private void TryApplyCliDefaultInstallPath(string previousCli, string currentCli)
|
|
{
|
|
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
var previousDefaultInstall = GetDefaultInstallDir(userHome, previousCli);
|
|
var currentDefaultInstall = GetDefaultInstallDir(userHome, currentCli);
|
|
|
|
if (string.IsNullOrWhiteSpace(_installDir) || PathsEqual(_installDir, previousDefaultInstall))
|
|
{
|
|
_installDir = currentDefaultInstall;
|
|
}
|
|
}
|
|
|
|
private static string GetDefaultInstallDir(string userHome, string cliType)
|
|
{
|
|
var baseDir = IsClaudeCli(cliType) ? ".claude" : ".codex";
|
|
return Path.Combine(userHome, baseDir, "skills/unity-mcp-skill");
|
|
}
|
|
|
|
private static bool IsClaudeCli(string cliType)
|
|
{
|
|
return string.Equals(cliType, ClaudeCli, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool PathsEqual(string left, string right)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
return string.Equals(
|
|
SkillSyncService.ExpandPath(left),
|
|
SkillSyncService.ExpandPath(right),
|
|
StringComparison.Ordinal);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void AppendLineImmediate(string line)
|
|
{
|
|
var sanitized = SanitizeLogLine(line);
|
|
if (string.IsNullOrWhiteSpace(sanitized))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_logBuilder.AppendLine($"[{DateTime.Now:HH:mm:ss}] {sanitized}");
|
|
_scroll.y = float.MaxValue;
|
|
Repaint();
|
|
}
|
|
|
|
private bool FlushPendingLogs()
|
|
{
|
|
var hasNewLine = false;
|
|
while (_pendingLogs.TryDequeue(out var line))
|
|
{
|
|
_logBuilder.AppendLine(line);
|
|
hasNewLine = true;
|
|
}
|
|
|
|
if (hasNewLine)
|
|
{
|
|
_scroll.y = float.MaxValue;
|
|
}
|
|
|
|
return hasNewLine;
|
|
}
|
|
|
|
private static string SanitizeLogLine(string line)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var sb = new StringBuilder(line.Length);
|
|
var inEscape = false;
|
|
foreach (var ch in line)
|
|
{
|
|
if (inEscape)
|
|
{
|
|
if (ch >= '@' && ch <= '~')
|
|
{
|
|
inEscape = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ch == '\u001b')
|
|
{
|
|
inEscape = true;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '\t' || (ch >= ' ' && ch != 127))
|
|
{
|
|
sb.Append(ch);
|
|
}
|
|
}
|
|
|
|
return sb.ToString().Trim();
|
|
}
|
|
}
|
|
}
|