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 } /// /// Synchronizes skill files when tools are enabled/disabled. /// Removes skill directories on disable, re-installs on enable. /// 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 DetectTargets() { return DetectTargets(requireSkillsDirectory: false); } public static List 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 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 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 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 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 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 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 DetectTargets(string projectRoot, bool requireSkillsDirectory) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); List 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 DetectTargets( string projectRoot, bool requireSkillsDirectory, bool groupSkillsUnderUnityCliLoop) { return DetectTargets( projectRoot, requireSkillsDirectory, groupSkillsUnderUnityCliLoop, includeFreshnessCheck: true); } internal static List DetectTargets( string projectRoot, bool requireSkillsDirectory, bool groupSkillsUnderUnityCliLoop, bool includeFreshnessCheck) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); List 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); } /// /// Re-installs skills only for targets that already opted in via an existing skills directory. /// public static async Task InstallSkillFiles() { return await InstallSkillFiles(groupSkillsUnderUnityCliLoop: false); } public static async Task InstallSkillFiles(bool groupSkillsUnderUnityCliLoop) { List targets = DetectTargets(requireSkillsDirectory: true); return await InstallSkillFiles(targets, groupSkillsUnderUnityCliLoop); } public static async Task 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 InstallSkillFiles(List targets) { return await InstallSkillFiles(targets, groupSkillsUnderUnityCliLoop: false); } public static async Task InstallSkillFiles( List 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 InstallSkillFilesAtProjectRoot( string projectRoot, IEnumerable 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 allSkills = SkillInstallLayout.GetSkillSourceInfos(projectRoot); List disabledSkills = allSkills .Where(skill => IsSkillDisabledByToolSettings( skill, disabledTools, ToolExecutionAvailability.IsTestFrameworkAvailable)) .ToList(); List 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 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 allSkills = SkillInstallLayout.GetSkillSourceInfos(projectRoot); List disabledSkills = allSkills .Where(skill => IsSkillDisabledByToolSettings( skill, disabledTools, ToolExecutionAvailability.IsTestFrameworkAvailable)) .ToList(); List 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 disabledSkills, IReadOnlyCollection enabledSkills, bool groupSkillsUnderUnityCliLoop) { string targetRoot = Path.Combine(projectRoot, target.DirName); string skillsRoot = SkillInstallLayout.GetSkillsRoot(targetRoot); HashSet 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 disabledSkills, IReadOnlyCollection 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 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 DetectTargetsWithSkillsDirectory(string projectRoot) { List 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 expectedSkillNames, IEnumerable removableSkillNames, bool groupSkillsUnderUnityCliLoop) { HashSet expectedSkillNameSet = new(expectedSkillNames, StringComparer.Ordinal); HashSet 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 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 backupFiles = skillDirectoryExisted ? ReadSkillFiles(skillDirectory) : new Dictionary(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 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 disabledSkills, bool groupSkillsUnderUnityCliLoop) { foreach (SkillInstallLayout.SkillSourceInfo skill in disabledSkills) { DeleteSkillDirectoryIfExists(targetRoot, skill.Name, groupSkillsUnderUnityCliLoop); } } private static void WriteSkillFiles( string skillDirectory, IReadOnlyDictionary skillFiles) { foreach (KeyValuePair 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 ReadSkillFiles(string skillDirectory) { Dictionary 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 expectedRelativePaths) { HashSet 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 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); } } } }