chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,974 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public enum SkillInstallState
|
||||
{
|
||||
Missing,
|
||||
Checking,
|
||||
Installed,
|
||||
Outdated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes skill files when tools are enabled/disabled.
|
||||
/// Removes skill directories on disable, re-installs on enable.
|
||||
/// </summary>
|
||||
public static class ToolSkillSynchronizer
|
||||
{
|
||||
public readonly struct SkillTargetDefinition
|
||||
{
|
||||
public readonly string DirName;
|
||||
public readonly string Flag;
|
||||
public readonly string DisplayName;
|
||||
|
||||
public SkillTargetDefinition(string dirName, string flag, string displayName)
|
||||
{
|
||||
DirName = dirName;
|
||||
Flag = flag;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct SkillInstallResult
|
||||
{
|
||||
public readonly int AttemptedTargets;
|
||||
public readonly int SucceededTargets;
|
||||
|
||||
public SkillInstallResult(int attemptedTargets, int succeededTargets)
|
||||
{
|
||||
AttemptedTargets = attemptedTargets;
|
||||
SucceededTargets = succeededTargets;
|
||||
}
|
||||
|
||||
public int FailedTargets => AttemptedTargets - SucceededTargets;
|
||||
public bool IsSuccessful => FailedTargets == 0;
|
||||
}
|
||||
|
||||
public readonly struct SkillTargetInfo
|
||||
{
|
||||
public readonly string DisplayName;
|
||||
public readonly string DirName;
|
||||
public readonly string InstallFlag;
|
||||
public readonly bool HasSkillsDirectory;
|
||||
public readonly bool HasExistingSkills;
|
||||
public readonly bool HasDifferentLayoutSkills;
|
||||
public readonly SkillInstallState InstallState;
|
||||
|
||||
public SkillTargetInfo(
|
||||
string displayName,
|
||||
string dirName,
|
||||
string installFlag,
|
||||
bool hasSkillsDirectory,
|
||||
bool hasExistingSkills,
|
||||
bool hasDifferentLayoutSkills = false,
|
||||
SkillInstallState installState = SkillInstallState.Missing)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
DirName = dirName;
|
||||
InstallFlag = installFlag;
|
||||
HasSkillsDirectory = hasSkillsDirectory;
|
||||
HasExistingSkills = hasExistingSkills;
|
||||
HasDifferentLayoutSkills = hasDifferentLayoutSkills;
|
||||
InstallState = installState;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly SkillTargetDefinition[] SkillTargets =
|
||||
{
|
||||
new(".claude", "--claude", "Claude Code"),
|
||||
new(".cursor", "--cursor", "Cursor"),
|
||||
new(".gemini", "--gemini", "Gemini CLI"),
|
||||
new(".codex", "--codex", "Codex CLI"),
|
||||
new(".agents", "--agents", "Other (.agents)"),
|
||||
new(".agent", "--antigravity", "Antigravity")
|
||||
};
|
||||
|
||||
private static readonly string[] DeprecatedSkillNames =
|
||||
{
|
||||
"uloop-capture-window",
|
||||
"uloop-get-provider-details",
|
||||
"uloop-unity-search",
|
||||
"uloop-get-menu-items",
|
||||
"uloop-get-unity-search-providers",
|
||||
"uloop-execute-menu-item"
|
||||
};
|
||||
private static readonly string[] ExcludedSkillFileNames =
|
||||
{
|
||||
".meta",
|
||||
".DS_Store",
|
||||
".gitkeep"
|
||||
};
|
||||
|
||||
internal static readonly string[] SkillTargetDirs = SkillTargets.Select(t => t.DirName).ToArray();
|
||||
|
||||
public static void RemoveSkillFiles(string toolName)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
RemoveSkillFilesAtProjectRoot(projectRoot, toolName);
|
||||
}
|
||||
|
||||
internal static void RemoveSkillFilesAtProjectRoot(string projectRoot, string toolName)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
foreach (string targetDir in SkillTargetDirs)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, targetDir);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string skillDir in SkillInstallLayout.EnumerateInstalledSkillDirectories(targetRoot))
|
||||
{
|
||||
if (SkillInstallLayout.SkillMatchesTool(skillDir, toolName))
|
||||
{
|
||||
Debug.Log($"[uLoopMCP] Removing skill '{toolName}' from '{skillDir}'");
|
||||
Directory.Delete(skillDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSkillInstalled(string toolName)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
|
||||
foreach (string targetDir in SkillTargetDirs)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, targetDir);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string skillDir in SkillInstallLayout.EnumerateInstalledSkillDirectories(targetRoot))
|
||||
{
|
||||
if (SkillInstallLayout.SkillMatchesTool(skillDir, toolName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<SkillTargetInfo> DetectTargets()
|
||||
{
|
||||
return DetectTargets(requireSkillsDirectory: false);
|
||||
}
|
||||
|
||||
public static List<SkillTargetInfo> DetectTargetsForLayout(bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargetsForLayoutAtProjectRoot(projectRoot, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
public static List<SkillTargetInfo> DetectTargetsForLayoutFast(bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargetsForLayoutFastAtProjectRoot(projectRoot, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargetsForLayoutAtProjectRoot(
|
||||
string projectRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargets(
|
||||
projectRoot,
|
||||
requireSkillsDirectory: false,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck: true);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargetsForLayoutFastAtProjectRoot(
|
||||
string projectRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargets(
|
||||
projectRoot,
|
||||
requireSkillsDirectory: false,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck: false);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(bool requireSkillsDirectory)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargets(projectRoot, requireSkillsDirectory);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(
|
||||
bool requireSkillsDirectory,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargets(
|
||||
projectRoot,
|
||||
requireSkillsDirectory,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck: true);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(
|
||||
bool requireSkillsDirectory,
|
||||
bool groupSkillsUnderUnityCliLoop,
|
||||
bool includeFreshnessCheck)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return DetectTargets(
|
||||
projectRoot,
|
||||
requireSkillsDirectory,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(string projectRoot, bool requireSkillsDirectory)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
List<SkillTargetInfo> targets = new();
|
||||
|
||||
foreach (SkillTargetDefinition target in SkillTargets)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, target.DirName);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasSkillsDirectory = SkillInstallLayout.HasOptedInSkillsDirectory(targetRoot);
|
||||
if (requireSkillsDirectory && !hasSkillsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasULoopSkills = hasSkillsDirectory
|
||||
&& SkillInstallLayout.HasInstalledSkills(projectRoot, targetRoot);
|
||||
targets.Add(new SkillTargetInfo(
|
||||
target.DisplayName,
|
||||
target.DirName,
|
||||
target.Flag,
|
||||
hasSkillsDirectory,
|
||||
hasULoopSkills,
|
||||
installState: hasULoopSkills
|
||||
? SkillInstallState.Installed
|
||||
: SkillInstallState.Missing));
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(
|
||||
string projectRoot,
|
||||
bool requireSkillsDirectory,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
return DetectTargets(
|
||||
projectRoot,
|
||||
requireSkillsDirectory,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck: true);
|
||||
}
|
||||
|
||||
internal static List<SkillTargetInfo> DetectTargets(
|
||||
string projectRoot,
|
||||
bool requireSkillsDirectory,
|
||||
bool groupSkillsUnderUnityCliLoop,
|
||||
bool includeFreshnessCheck)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
List<SkillTargetInfo> targets = new();
|
||||
|
||||
foreach (SkillTargetDefinition target in SkillTargets)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, target.DirName);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasSkillsDirectory = SkillInstallLayout.HasOptedInSkillsDirectory(targetRoot);
|
||||
if (requireSkillsDirectory && !hasSkillsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SkillInstallState installState = ResolveInstallState(
|
||||
projectRoot,
|
||||
targetRoot,
|
||||
hasSkillsDirectory,
|
||||
groupSkillsUnderUnityCliLoop,
|
||||
includeFreshnessCheck);
|
||||
bool hasULoopSkills = installState == SkillInstallState.Installed
|
||||
|| installState == SkillInstallState.Checking
|
||||
|| installState == SkillInstallState.Outdated;
|
||||
bool hasDifferentLayoutSkills = hasSkillsDirectory
|
||||
&& SkillInstallLayout.HasInstalledSkills(projectRoot, targetRoot, !groupSkillsUnderUnityCliLoop);
|
||||
targets.Add(new SkillTargetInfo(
|
||||
target.DisplayName,
|
||||
target.DirName,
|
||||
target.Flag,
|
||||
hasSkillsDirectory,
|
||||
hasULoopSkills,
|
||||
hasDifferentLayoutSkills,
|
||||
installState));
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static SkillInstallState ResolveInstallState(
|
||||
string projectRoot,
|
||||
string targetRoot,
|
||||
bool hasSkillsDirectory,
|
||||
bool groupSkillsUnderUnityCliLoop,
|
||||
bool includeFreshnessCheck)
|
||||
{
|
||||
if (!hasSkillsDirectory)
|
||||
{
|
||||
return SkillInstallState.Missing;
|
||||
}
|
||||
|
||||
if (!includeFreshnessCheck)
|
||||
{
|
||||
return SkillInstallLayout.HasInstalledSkills(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop)
|
||||
? SkillInstallState.Checking
|
||||
: SkillInstallState.Missing;
|
||||
}
|
||||
|
||||
return SkillInstallLayout.GetInstalledState(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-installs skills only for targets that already opted in via an existing skills directory.
|
||||
/// </summary>
|
||||
public static async Task<SkillInstallResult> InstallSkillFiles()
|
||||
{
|
||||
return await InstallSkillFiles(groupSkillsUnderUnityCliLoop: false);
|
||||
}
|
||||
|
||||
public static async Task<SkillInstallResult> InstallSkillFiles(bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
List<SkillTargetInfo> targets = DetectTargets(requireSkillsDirectory: true);
|
||||
return await InstallSkillFiles(targets, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
public static async Task<SkillInstallResult> InstallSkillFilesForTool(
|
||||
string toolName,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return await InstallSkillFilesForToolAtProjectRoot(
|
||||
projectRoot,
|
||||
toolName,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
public static async Task<SkillInstallResult> InstallSkillFiles(List<SkillTargetInfo> targets)
|
||||
{
|
||||
return await InstallSkillFiles(targets, groupSkillsUnderUnityCliLoop: false);
|
||||
}
|
||||
|
||||
public static async Task<SkillInstallResult> InstallSkillFiles(
|
||||
List<SkillTargetInfo> targets,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(targets != null, "targets must not be null");
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
|
||||
return await InstallSkillFilesAtProjectRoot(projectRoot, targets, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
internal static async Task<SkillInstallResult> InstallSkillFilesAtProjectRoot(
|
||||
string projectRoot,
|
||||
IEnumerable<SkillTargetInfo> targets,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
Debug.Assert(targets != null, "targets must not be null");
|
||||
|
||||
SkillTargetInfo[] targetArray = targets.ToArray();
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
string[] disabledTools = ToolSettings.GetDisabledTools();
|
||||
List<SkillInstallLayout.SkillSourceInfo> allSkills = SkillInstallLayout.GetSkillSourceInfos(projectRoot);
|
||||
List<SkillInstallLayout.SkillSourceInfo> disabledSkills = allSkills
|
||||
.Where(skill => IsSkillDisabledByToolSettings(
|
||||
skill,
|
||||
disabledTools,
|
||||
ToolExecutionAvailability.IsTestFrameworkAvailable))
|
||||
.ToList();
|
||||
List<SkillInstallLayout.SkillSourceInfo> enabledSkills = allSkills
|
||||
.Except(disabledSkills)
|
||||
.ToList();
|
||||
|
||||
int succeeded = 0;
|
||||
foreach (SkillTargetInfo target in targetArray)
|
||||
{
|
||||
InstallSkillsForTarget(
|
||||
projectRoot,
|
||||
target,
|
||||
disabledSkills,
|
||||
enabledSkills,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
succeeded++;
|
||||
}
|
||||
|
||||
return new SkillInstallResult(targetArray.Length, succeeded);
|
||||
});
|
||||
}
|
||||
|
||||
internal static async Task<SkillInstallResult> InstallSkillFilesForToolAtProjectRoot(
|
||||
string projectRoot,
|
||||
string toolName,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
SkillTargetInfo[] targetArray = DetectTargetsWithSkillsDirectory(projectRoot).ToArray();
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
string[] disabledTools = ToolSettings.GetDisabledTools();
|
||||
List<SkillInstallLayout.SkillSourceInfo> allSkills = SkillInstallLayout.GetSkillSourceInfos(projectRoot);
|
||||
List<SkillInstallLayout.SkillSourceInfo> disabledSkills = allSkills
|
||||
.Where(skill => IsSkillDisabledByToolSettings(
|
||||
skill,
|
||||
disabledTools,
|
||||
ToolExecutionAvailability.IsTestFrameworkAvailable))
|
||||
.ToList();
|
||||
List<SkillInstallLayout.SkillSourceInfo> toolSkills = allSkills
|
||||
.Where(skill => IsSkillForTool(skill, toolName))
|
||||
.ToList();
|
||||
if (toolSkills.Count == 0)
|
||||
{
|
||||
return new SkillInstallResult(0, 0);
|
||||
}
|
||||
|
||||
int succeeded = 0;
|
||||
foreach (SkillTargetInfo target in targetArray)
|
||||
{
|
||||
InstallSpecificSkillsForTarget(
|
||||
projectRoot,
|
||||
target,
|
||||
disabledSkills,
|
||||
toolSkills,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
succeeded++;
|
||||
}
|
||||
|
||||
return new SkillInstallResult(targetArray.Length, succeeded);
|
||||
});
|
||||
}
|
||||
|
||||
private static void InstallSkillsForTarget(
|
||||
string projectRoot,
|
||||
SkillTargetInfo target,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> disabledSkills,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> enabledSkills,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, target.DirName);
|
||||
string skillsRoot = SkillInstallLayout.GetSkillsRoot(targetRoot);
|
||||
HashSet<string> managedSkillNames = new(
|
||||
enabledSkills.Select(skill => skill.Name),
|
||||
StringComparer.Ordinal);
|
||||
managedSkillNames.UnionWith(disabledSkills.Select(skill => skill.Name));
|
||||
managedSkillNames.UnionWith(DeprecatedSkillNames);
|
||||
Directory.CreateDirectory(skillsRoot);
|
||||
|
||||
if (groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Directory.CreateDirectory(SkillInstallLayout.GetManagedSkillsRoot(targetRoot));
|
||||
}
|
||||
|
||||
DeleteDeprecatedSkillDirectoriesFromAllLayouts(targetRoot);
|
||||
DeleteDisabledSkillDirectoriesFromAllLayouts(targetRoot, disabledSkills);
|
||||
|
||||
foreach (SkillInstallLayout.SkillSourceInfo skill in enabledSkills)
|
||||
{
|
||||
string installedSkillDirectory = SkillInstallLayout.GetInstalledSkillDirectoryPathForLayout(
|
||||
targetRoot,
|
||||
skill.Name,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
SyncInstalledSkillDirectory(installedSkillDirectory, skill.SkillFiles);
|
||||
DeleteSkillDirectoryIfExists(targetRoot, skill.Name, !groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
DeleteUnexpectedInstalledSkillDirectories(
|
||||
targetRoot,
|
||||
enabledSkills.Select(skill => skill.Name),
|
||||
managedSkillNames,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
|
||||
if (!groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
DeleteUnexpectedInstalledSkillDirectories(
|
||||
targetRoot,
|
||||
enabledSkills.Select(skill => skill.Name),
|
||||
managedSkillNames,
|
||||
groupSkillsUnderUnityCliLoop: true);
|
||||
DeleteEmptyManagedSkillsParentDirectoryIfNeeded(
|
||||
targetRoot,
|
||||
groupSkillsUnderUnityCliLoop: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InstallSpecificSkillsForTarget(
|
||||
string projectRoot,
|
||||
SkillTargetInfo target,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> disabledSkills,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> skills,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, target.DirName);
|
||||
string skillsRoot = SkillInstallLayout.GetSkillsRoot(targetRoot);
|
||||
Directory.CreateDirectory(skillsRoot);
|
||||
|
||||
if (groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Directory.CreateDirectory(SkillInstallLayout.GetManagedSkillsRoot(targetRoot));
|
||||
}
|
||||
|
||||
DeleteDeprecatedSkillDirectoriesForLayout(targetRoot, groupSkillsUnderUnityCliLoop);
|
||||
DeleteDisabledSkillDirectoriesForLayout(targetRoot, disabledSkills, groupSkillsUnderUnityCliLoop);
|
||||
|
||||
foreach (SkillInstallLayout.SkillSourceInfo skill in skills)
|
||||
{
|
||||
string installedSkillDirectory = SkillInstallLayout.GetInstalledSkillDirectoryPathForLayout(
|
||||
targetRoot,
|
||||
skill.Name,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
SyncInstalledSkillDirectory(installedSkillDirectory, skill.SkillFiles);
|
||||
DeleteSkillDirectoryIfExists(targetRoot, skill.Name, !groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
if (!groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
DeleteEmptyManagedSkillsParentDirectoryIfNeeded(
|
||||
targetRoot,
|
||||
groupSkillsUnderUnityCliLoop: true);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsSkillDisabledByToolSettings(
|
||||
SkillInstallLayout.SkillSourceInfo skill,
|
||||
IReadOnlyCollection<string> disabledTools,
|
||||
bool isTestFrameworkAvailable)
|
||||
{
|
||||
if (disabledTools.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string toolName = GetToolNameForSkill(skill);
|
||||
if (string.IsNullOrEmpty(toolName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return disabledTools.Contains(toolName)
|
||||
&& !ToolExecutionAvailability.ShouldReportDependencyUnavailableBeforeDisabled(
|
||||
toolName,
|
||||
isTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
private static bool IsSkillForTool(
|
||||
SkillInstallLayout.SkillSourceInfo skill,
|
||||
string toolName)
|
||||
{
|
||||
string skillToolName = GetToolNameForSkill(skill);
|
||||
return string.Equals(skillToolName, toolName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string GetToolNameForSkill(SkillInstallLayout.SkillSourceInfo skill)
|
||||
{
|
||||
string toolName = skill.ToolName;
|
||||
if (string.IsNullOrEmpty(toolName) && skill.Name.StartsWith(CliConstants.SKILL_DIR_PREFIX, StringComparison.Ordinal))
|
||||
{
|
||||
toolName = skill.Name.Substring(CliConstants.SKILL_DIR_PREFIX.Length);
|
||||
}
|
||||
|
||||
return toolName;
|
||||
}
|
||||
|
||||
private static List<SkillTargetInfo> DetectTargetsWithSkillsDirectory(string projectRoot)
|
||||
{
|
||||
List<SkillTargetInfo> targets = new();
|
||||
foreach (SkillTargetDefinition target in SkillTargets)
|
||||
{
|
||||
string targetRoot = Path.Combine(projectRoot, target.DirName);
|
||||
if (!Directory.Exists(targetRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasSkillsDirectory = SkillInstallLayout.HasOptedInSkillsDirectory(targetRoot);
|
||||
if (!hasSkillsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.Add(new SkillTargetInfo(
|
||||
target.DisplayName,
|
||||
target.DirName,
|
||||
target.Flag,
|
||||
hasSkillsDirectory,
|
||||
hasExistingSkills: false));
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static void DeleteUnexpectedInstalledSkillDirectories(
|
||||
string targetRoot,
|
||||
IEnumerable<string> expectedSkillNames,
|
||||
IEnumerable<string> removableSkillNames,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
HashSet<string> expectedSkillNameSet = new(expectedSkillNames, StringComparer.Ordinal);
|
||||
HashSet<string> removableSkillNameSet = new(removableSkillNames, StringComparer.Ordinal);
|
||||
foreach (string installedSkillName in SkillInstallLayout.EnumerateInstalledSkillDirectoryNamesForLayout(
|
||||
targetRoot,
|
||||
groupSkillsUnderUnityCliLoop))
|
||||
{
|
||||
if (expectedSkillNameSet.Contains(installedSkillName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!removableSkillNameSet.Contains(installedSkillName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DeleteSkillDirectoryIfExists(targetRoot, installedSkillName, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncInstalledSkillDirectory(
|
||||
string skillDirectory,
|
||||
IReadOnlyDictionary<string, byte[]> skillFiles)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(skillDirectory), "skillDirectory must not be null or empty");
|
||||
Debug.Assert(skillFiles != null, "skillFiles must not be null");
|
||||
Debug.Assert(skillFiles.ContainsKey(SkillInstallLayout.SkillFileName),
|
||||
"skillFiles must contain SKILL.md");
|
||||
|
||||
string parentDirectory = Path.GetDirectoryName(skillDirectory);
|
||||
Debug.Assert(!string.IsNullOrEmpty(parentDirectory), "parentDirectory must not be null or empty");
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
bool skillDirectoryExisted = Directory.Exists(skillDirectory);
|
||||
Dictionary<string, byte[]> backupFiles = skillDirectoryExisted
|
||||
? ReadSkillFiles(skillDirectory)
|
||||
: new Dictionary<string, byte[]>(StringComparer.Ordinal);
|
||||
Directory.CreateDirectory(skillDirectory);
|
||||
|
||||
bool syncCompleted = false;
|
||||
try
|
||||
{
|
||||
WriteSkillFiles(skillDirectory, skillFiles);
|
||||
DeleteUnexpectedSkillFiles(skillDirectory, skillFiles.Keys);
|
||||
DeleteEmptyDirectories(skillDirectory);
|
||||
syncCompleted = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!syncCompleted)
|
||||
{
|
||||
RollbackSkillDirectory(skillDirectory, backupFiles, skillDirectoryExisted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDeprecatedSkillDirectoriesFromAllLayouts(string targetRoot)
|
||||
{
|
||||
foreach (string deprecatedSkillName in DeprecatedSkillNames)
|
||||
{
|
||||
DeleteSkillDirectoryIfExists(targetRoot, deprecatedSkillName, groupSkillsUnderUnityCliLoop: true);
|
||||
DeleteSkillDirectoryIfExists(targetRoot, deprecatedSkillName, groupSkillsUnderUnityCliLoop: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDisabledSkillDirectoriesFromAllLayouts(
|
||||
string targetRoot,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> disabledSkills)
|
||||
{
|
||||
foreach (SkillInstallLayout.SkillSourceInfo skill in disabledSkills)
|
||||
{
|
||||
DeleteSkillDirectoryIfExists(targetRoot, skill.Name, groupSkillsUnderUnityCliLoop: true);
|
||||
DeleteSkillDirectoryIfExists(targetRoot, skill.Name, groupSkillsUnderUnityCliLoop: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDeprecatedSkillDirectoriesForLayout(
|
||||
string targetRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
foreach (string deprecatedSkillName in DeprecatedSkillNames)
|
||||
{
|
||||
DeleteSkillDirectoryIfExists(targetRoot, deprecatedSkillName, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDisabledSkillDirectoriesForLayout(
|
||||
string targetRoot,
|
||||
IReadOnlyCollection<SkillInstallLayout.SkillSourceInfo> disabledSkills,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
foreach (SkillInstallLayout.SkillSourceInfo skill in disabledSkills)
|
||||
{
|
||||
DeleteSkillDirectoryIfExists(targetRoot, skill.Name, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteSkillFiles(
|
||||
string skillDirectory,
|
||||
IReadOnlyDictionary<string, byte[]> skillFiles)
|
||||
{
|
||||
foreach (KeyValuePair<string, byte[]> skillFile in skillFiles)
|
||||
{
|
||||
string fullPath = Path.Combine(skillDirectory, skillFile.Key);
|
||||
string fileDirectory = Path.GetDirectoryName(fullPath);
|
||||
Debug.Assert(!string.IsNullOrEmpty(fileDirectory), "fileDirectory must not be null or empty");
|
||||
Directory.CreateDirectory(fileDirectory);
|
||||
if (File.Exists(fullPath) && File.ReadAllBytes(fullPath).SequenceEqual(skillFile.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
WriteFileAtomically(fullPath, skillFile.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, byte[]> ReadSkillFiles(string skillDirectory)
|
||||
{
|
||||
Dictionary<string, byte[]> files = new(StringComparer.Ordinal);
|
||||
foreach (string filePath in Directory.EnumerateFiles(skillDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
if (IsExcludedSkillFile(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string relativePath = Path.GetRelativePath(skillDirectory, filePath);
|
||||
files[relativePath] = File.ReadAllBytes(filePath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static void WriteFileAtomically(string fullPath, byte[] content)
|
||||
{
|
||||
string fileDirectory = Path.GetDirectoryName(fullPath);
|
||||
Debug.Assert(!string.IsNullOrEmpty(fileDirectory), "fileDirectory must not be null or empty");
|
||||
|
||||
string tempPath = Path.Combine(
|
||||
fileDirectory,
|
||||
$"{Path.GetFileName(fullPath)}.tmp-{Guid.NewGuid():N}");
|
||||
File.WriteAllBytes(tempPath, content);
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Replace(tempPath, fullPath, null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
File.Move(tempPath, fullPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteUnexpectedSkillFiles(
|
||||
string skillDirectory,
|
||||
IEnumerable<string> expectedRelativePaths)
|
||||
{
|
||||
HashSet<string> expectedPaths = new(expectedRelativePaths, StringComparer.Ordinal);
|
||||
foreach (string filePath in Directory.EnumerateFiles(skillDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
if (IsExcludedSkillFile(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string relativePath = Path.GetRelativePath(skillDirectory, filePath);
|
||||
if (expectedPaths.Contains(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEmptyDirectories(string skillDirectory)
|
||||
{
|
||||
foreach (string directoryPath in Directory.EnumerateDirectories(skillDirectory, "*", SearchOption.AllDirectories)
|
||||
.OrderByDescending(path => path.Length))
|
||||
{
|
||||
if (Directory.EnumerateFileSystemEntries(directoryPath).Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.Delete(directoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RollbackSkillDirectory(
|
||||
string skillDirectory,
|
||||
IReadOnlyDictionary<string, byte[]> backupFiles,
|
||||
bool skillDirectoryExisted)
|
||||
{
|
||||
if (!skillDirectoryExisted)
|
||||
{
|
||||
if (Directory.Exists(skillDirectory))
|
||||
{
|
||||
Directory.Delete(skillDirectory, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(skillDirectory);
|
||||
WriteSkillFiles(skillDirectory, backupFiles);
|
||||
DeleteUnexpectedSkillFiles(skillDirectory, backupFiles.Keys);
|
||||
DeleteEmptyDirectories(skillDirectory);
|
||||
}
|
||||
|
||||
private static bool IsExcludedSkillFile(string fileName)
|
||||
{
|
||||
if (ExcludedSkillFileNames.Contains(fileName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (string excludedPattern in ExcludedSkillFileNames)
|
||||
{
|
||||
if (fileName.EndsWith(excludedPattern, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void DeleteSkillDirectoryIfExists(
|
||||
string targetRoot,
|
||||
string skillName,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string installedSkillDirectory = SkillInstallLayout.GetInstalledSkillDirectoryPathForLayout(
|
||||
targetRoot,
|
||||
skillName,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
if (!Directory.Exists(installedSkillDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(installedSkillDirectory, true);
|
||||
DeleteEmptyManagedSkillsParentDirectoryIfNeeded(targetRoot, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
private static void DeleteEmptyManagedSkillsParentDirectoryIfNeeded(
|
||||
string targetRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
if (!groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string managedSkillsRoot = SkillInstallLayout.GetManagedSkillsRoot(targetRoot);
|
||||
if (!Directory.Exists(managedSkillsRoot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeleteExcludedFilesAtRoot(managedSkillsRoot);
|
||||
DeleteEmptyDirectoriesAtRoot(managedSkillsRoot);
|
||||
if (Directory.EnumerateFileSystemEntries(managedSkillsRoot).Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(managedSkillsRoot);
|
||||
}
|
||||
|
||||
private static void DeleteExcludedFilesAtRoot(string directoryPath)
|
||||
{
|
||||
foreach (string filePath in Directory.EnumerateFiles(directoryPath))
|
||||
{
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
if (!IsExcludedSkillFile(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEmptyDirectoriesAtRoot(string directoryPath)
|
||||
{
|
||||
foreach (string childDirectoryPath in Directory.EnumerateDirectories(directoryPath))
|
||||
{
|
||||
DeleteEmptyDirectories(childDirectoryPath);
|
||||
if (Directory.EnumerateFileSystemEntries(childDirectoryPath).Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.Delete(childDirectoryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user