chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,806 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class SkillInstallLayout
|
||||
{
|
||||
internal const string SkillsDirName = "skills";
|
||||
internal const string ManagedSkillsDirName = "unity-cli-loop";
|
||||
internal const string SkillFileName = "SKILL.md";
|
||||
private const string CliPackageDirName = "Cli~";
|
||||
private const string CliSkillDefinitionsDirName = "skill-definitions";
|
||||
private const string CliOnlySkillDefinitionsDirName = "cli-only";
|
||||
private const string MarkdownFileExtension = ".md";
|
||||
private static readonly HashSet<string> ExcludedFileNames = new()
|
||||
{
|
||||
".meta",
|
||||
".DS_Store",
|
||||
".gitkeep"
|
||||
};
|
||||
private static readonly Regex UloopCommandPattern = new(
|
||||
"(^|[^A-Za-z0-9_-])uloop(?=\\s)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private sealed class SkillSourceDefinition
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string ToolName;
|
||||
public readonly string SkillDirectoryPath;
|
||||
public readonly Dictionary<string, byte[]> SkillFiles;
|
||||
|
||||
public SkillSourceDefinition(
|
||||
string name,
|
||||
string toolName,
|
||||
string skillDirectoryPath,
|
||||
Dictionary<string, byte[]> skillFiles)
|
||||
{
|
||||
Name = name;
|
||||
ToolName = toolName;
|
||||
SkillDirectoryPath = skillDirectoryPath;
|
||||
SkillFiles = skillFiles;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct SkillSourceInfo
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string ToolName;
|
||||
public readonly Dictionary<string, byte[]> SkillFiles;
|
||||
|
||||
public SkillSourceInfo(
|
||||
string name,
|
||||
string toolName,
|
||||
Dictionary<string, byte[]> skillFiles)
|
||||
{
|
||||
Name = name;
|
||||
ToolName = toolName;
|
||||
SkillFiles = skillFiles;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetSkillsRoot(string targetRoot)
|
||||
{
|
||||
return Path.Combine(targetRoot, SkillsDirName);
|
||||
}
|
||||
|
||||
internal static string GetManagedSkillsRoot(string targetRoot)
|
||||
{
|
||||
return Path.Combine(GetSkillsRoot(targetRoot), ManagedSkillsDirName);
|
||||
}
|
||||
|
||||
internal static bool HasOptedInSkillsDirectory(string targetRoot)
|
||||
{
|
||||
return Directory.Exists(GetSkillsRoot(targetRoot));
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> EnumerateInstalledSkillDirectories(string targetRoot)
|
||||
{
|
||||
foreach (string skillDir in EnumerateManagedSkillDirectories(targetRoot))
|
||||
{
|
||||
yield return skillDir;
|
||||
}
|
||||
|
||||
foreach (string skillDir in EnumerateLegacyManagedSkillDirectories(targetRoot))
|
||||
{
|
||||
yield return skillDir;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool HasInstalledSkills(string targetRoot)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
return HasInstalledSkills(projectRoot, targetRoot);
|
||||
}
|
||||
|
||||
internal static bool HasInstalledSkills(string targetRoot, bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
return HasInstalledSkills(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
internal static bool HasInstalledSkills(string projectRoot, string targetRoot)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(targetRoot), "targetRoot must not be null or empty");
|
||||
|
||||
return HasInstalledSkills(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop: false)
|
||||
|| HasInstalledSkills(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop: true);
|
||||
}
|
||||
|
||||
internal static bool HasInstalledSkills(
|
||||
string projectRoot,
|
||||
string targetRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(targetRoot), "targetRoot must not be null or empty");
|
||||
|
||||
if (!groupSkillsUnderUnityCliLoop && EnumerateLegacyManagedSkillDirectories(targetRoot).Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Dictionary<string, SkillSourceDefinition> expectedSkills = GetSkillSources(projectRoot);
|
||||
if (expectedSkills.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return expectedSkills.Keys.Any(skillName =>
|
||||
Directory.Exists(GetInstalledSkillDirectoryPath(targetRoot, skillName, groupSkillsUnderUnityCliLoop)));
|
||||
}
|
||||
|
||||
internal static SkillInstallState GetInstalledState(
|
||||
string projectRoot,
|
||||
string targetRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Dictionary<string, SkillSourceDefinition> expectedSkills = GetSkillSources(projectRoot);
|
||||
bool hasLayoutSkills = HasInstalledSkills(projectRoot, targetRoot, groupSkillsUnderUnityCliLoop);
|
||||
if (expectedSkills.Count == 0)
|
||||
{
|
||||
return hasLayoutSkills ? SkillInstallState.Installed : SkillInstallState.Missing;
|
||||
}
|
||||
|
||||
bool hasInstalledExpectedSkill = false;
|
||||
bool hasMissingExpectedSkill = false;
|
||||
|
||||
foreach (SkillSourceDefinition expectedSkill in expectedSkills.Values)
|
||||
{
|
||||
string installedSkillDirectory = GetInstalledSkillDirectoryPath(
|
||||
targetRoot,
|
||||
expectedSkill.Name,
|
||||
groupSkillsUnderUnityCliLoop);
|
||||
if (!Directory.Exists(installedSkillDirectory))
|
||||
{
|
||||
hasMissingExpectedSkill = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
hasInstalledExpectedSkill = true;
|
||||
if (IsSkillDirectoryOutdated(expectedSkill.SkillFiles, installedSkillDirectory))
|
||||
{
|
||||
return SkillInstallState.Outdated;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInstalledExpectedSkill)
|
||||
{
|
||||
return hasLayoutSkills ? SkillInstallState.Outdated : SkillInstallState.Missing;
|
||||
}
|
||||
|
||||
return hasMissingExpectedSkill ? SkillInstallState.Outdated : SkillInstallState.Installed;
|
||||
}
|
||||
|
||||
internal static bool SkillMatchesTool(string skillDir, string toolName)
|
||||
{
|
||||
string skillMdPath = Path.Combine(skillDir, SkillFileName);
|
||||
if (File.Exists(skillMdPath))
|
||||
{
|
||||
string content = File.ReadAllText(skillMdPath);
|
||||
string parsed = ParseToolNameFromFrontmatter(content);
|
||||
if (!string.IsNullOrEmpty(parsed))
|
||||
{
|
||||
return parsed == toolName;
|
||||
}
|
||||
}
|
||||
|
||||
string dirName = Path.GetFileName(skillDir);
|
||||
return dirName == $"{CliConstants.SKILL_DIR_PREFIX}{toolName}";
|
||||
}
|
||||
|
||||
internal static List<SkillSourceInfo> GetSkillSourceInfos(string projectRoot)
|
||||
{
|
||||
return GetSkillSources(projectRoot)
|
||||
.Values
|
||||
.Select(source => new SkillSourceInfo(source.Name, source.ToolName, source.SkillFiles))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static string GetInstalledSkillDirectoryPathForLayout(
|
||||
string targetRoot,
|
||||
string skillName,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
return GetInstalledSkillDirectoryPath(targetRoot, skillName, groupSkillsUnderUnityCliLoop);
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> EnumerateInstalledSkillDirectoryNamesForLayout(
|
||||
string targetRoot,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
IEnumerable<string> installedSkillDirectories = groupSkillsUnderUnityCliLoop
|
||||
? EnumerateManagedSkillDirectories(targetRoot)
|
||||
: EnumerateLegacyManagedSkillDirectories(targetRoot);
|
||||
return installedSkillDirectories
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrEmpty(name));
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateManagedSkillDirectories(string targetRoot)
|
||||
{
|
||||
string managedSkillsRoot = GetManagedSkillsRoot(targetRoot);
|
||||
if (!Directory.Exists(managedSkillsRoot))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
return Directory.EnumerateDirectories(managedSkillsRoot);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateLegacyManagedSkillDirectories(string targetRoot)
|
||||
{
|
||||
string skillsRoot = GetSkillsRoot(targetRoot);
|
||||
if (!Directory.Exists(skillsRoot))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
return Directory.EnumerateDirectories(skillsRoot)
|
||||
.Where(skillDir => Path.GetFileName(skillDir) != ManagedSkillsDirName)
|
||||
.Where(IsLegacyManagedSkillDirectory);
|
||||
}
|
||||
|
||||
private static bool IsLegacyManagedSkillDirectory(string skillDir)
|
||||
{
|
||||
string dirName = Path.GetFileName(skillDir);
|
||||
if (dirName.StartsWith(CliConstants.SKILL_DIR_PREFIX, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string skillMdPath = Path.Combine(skillDir, SkillFileName);
|
||||
if (!File.Exists(skillMdPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(skillMdPath);
|
||||
if (!string.IsNullOrEmpty(ParseToolNameFromFrontmatter(content)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetInstalledSkillDirectoryPath(
|
||||
string targetRoot,
|
||||
string skillName,
|
||||
bool groupSkillsUnderUnityCliLoop)
|
||||
{
|
||||
Debug.Assert(IsSafeSkillPathComponent(skillName), "skillName must be a single safe path component");
|
||||
|
||||
string skillsRoot = groupSkillsUnderUnityCliLoop
|
||||
? GetManagedSkillsRoot(targetRoot)
|
||||
: GetSkillsRoot(targetRoot);
|
||||
return Path.Combine(skillsRoot, skillName);
|
||||
}
|
||||
|
||||
private static bool IsSkillDirectoryOutdated(
|
||||
Dictionary<string, byte[]> sourceFiles,
|
||||
string installedSkillDirectory)
|
||||
{
|
||||
Dictionary<string, byte[]> installedFiles = CollectInstalledSkillFiles(installedSkillDirectory);
|
||||
if (sourceFiles.Count != installedFiles.Count)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, byte[]> sourceFile in sourceFiles)
|
||||
{
|
||||
if (!installedFiles.TryGetValue(sourceFile.Key, out byte[] installedContent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sourceFile.Value.SequenceEqual(installedContent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<string, byte[]> CollectInstalledSkillFiles(string skillDirectory)
|
||||
{
|
||||
Dictionary<string, byte[]> files = new(StringComparer.Ordinal);
|
||||
foreach (string filePath in Directory.EnumerateFiles(skillDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
if (IsExcludedFile(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string relativePath = Path.GetRelativePath(skillDirectory, filePath);
|
||||
files[relativePath] = File.ReadAllBytes(filePath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static Dictionary<string, byte[]> CollectSourceSkillFiles(
|
||||
string skillDirectory,
|
||||
string skillFilePath)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(skillDirectory), "skillDirectory must not be null or empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(skillFilePath), "skillFilePath must not be null or empty");
|
||||
|
||||
Dictionary<string, byte[]> sourceFiles;
|
||||
if (string.Equals(Path.GetFileName(skillDirectory), "Skill", StringComparison.Ordinal))
|
||||
{
|
||||
sourceFiles = CollectInstalledSkillFiles(skillDirectory);
|
||||
return FormatSkillFilesForCliInvocation(sourceFiles);
|
||||
}
|
||||
|
||||
sourceFiles = new Dictionary<string, byte[]>(StringComparer.Ordinal)
|
||||
{
|
||||
[SkillFileName] = File.ReadAllBytes(skillFilePath)
|
||||
};
|
||||
return FormatSkillFilesForCliInvocation(sourceFiles);
|
||||
}
|
||||
|
||||
private static Dictionary<string, byte[]> FormatSkillFilesForCliInvocation(
|
||||
Dictionary<string, byte[]> skillFiles)
|
||||
{
|
||||
Debug.Assert(skillFiles != null, "skillFiles must not be null");
|
||||
|
||||
if (ToolSettings.GetSkillCliInvocation() == CliConstants.SKILL_CLI_INVOCATION_GLOBAL)
|
||||
{
|
||||
return skillFiles;
|
||||
}
|
||||
|
||||
Debug.Assert(skillFiles.ContainsKey(SkillFileName), "skillFiles must contain SKILL.md");
|
||||
foreach (string relativePath in skillFiles.Keys.ToArray())
|
||||
{
|
||||
if (!IsMarkdownSkillFile(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string skillContent = Encoding.UTF8.GetString(skillFiles[relativePath]);
|
||||
string formattedContent = FormatSkillMarkdownForNpx(
|
||||
skillContent,
|
||||
McpConstants.PackageInfo.version);
|
||||
skillFiles[relativePath] = Encoding.UTF8.GetBytes(formattedContent);
|
||||
}
|
||||
return skillFiles;
|
||||
}
|
||||
|
||||
private static bool IsMarkdownSkillFile(string relativePath)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(relativePath), "relativePath must not be null or empty");
|
||||
return string.Equals(
|
||||
Path.GetExtension(relativePath),
|
||||
MarkdownFileExtension,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string FormatSkillMarkdownForNpx(string content, string packageVersion)
|
||||
{
|
||||
Debug.Assert(content != null, "content must not be null");
|
||||
Debug.Assert(!string.IsNullOrEmpty(packageVersion), "packageVersion must not be null or empty");
|
||||
|
||||
string npxCommand =
|
||||
$"{CliConstants.NPX_EXECUTABLE_NAME} {CliConstants.NPX_YES_FLAG} {CliConstants.NPM_PACKAGE_NAME}@{packageVersion}";
|
||||
Match frontmatterMatch = Regex.Match(
|
||||
content,
|
||||
"^(---\\r?\\n[\\s\\S]*?\\r?\\n---)(\\r?\\n?)([\\s\\S]*)$");
|
||||
if (!frontmatterMatch.Success)
|
||||
{
|
||||
return UloopCommandPattern.Replace(content, match => $"{match.Groups[1].Value}{npxCommand}");
|
||||
}
|
||||
|
||||
string frontmatter = $"{frontmatterMatch.Groups[1].Value}{frontmatterMatch.Groups[2].Value}";
|
||||
string body = frontmatterMatch.Groups[3].Value;
|
||||
string formattedBody = UloopCommandPattern.Replace(
|
||||
body,
|
||||
match => $"{match.Groups[1].Value}{npxCommand}");
|
||||
return $"{frontmatter}{formattedBody}";
|
||||
}
|
||||
|
||||
private static Dictionary<string, SkillSourceDefinition> GetSkillSources(string projectRoot)
|
||||
{
|
||||
Dictionary<string, SkillSourceDefinition> sources = new(StringComparer.Ordinal);
|
||||
foreach (string searchRoot in EnumerateSkillSourceRoots(projectRoot))
|
||||
{
|
||||
if (!Directory.Exists(searchRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IEnumerable<string> skillFilePaths = IsCliOnlySkillSourceRoot(searchRoot)
|
||||
? Directory.EnumerateFiles(searchRoot, SkillFileName, SearchOption.AllDirectories)
|
||||
: EnumerateEditorFolders(searchRoot, 3).SelectMany(editorFolder =>
|
||||
Directory.EnumerateFiles(editorFolder, SkillFileName, SearchOption.AllDirectories));
|
||||
foreach (string skillFilePath in skillFilePaths)
|
||||
{
|
||||
string skillDirectory = Path.GetDirectoryName(skillFilePath);
|
||||
if (skillDirectory == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string skillContent = File.ReadAllText(skillFilePath);
|
||||
if (IsInternalSkill(skillContent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string skillName = ParseNameFromFrontmatter(skillContent);
|
||||
if (string.IsNullOrEmpty(skillName)
|
||||
|| !IsSafeSkillPathComponent(skillName)
|
||||
|| sources.ContainsKey(skillName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sources[skillName] = new SkillSourceDefinition(
|
||||
skillName,
|
||||
ParseToolNameFromFrontmatter(skillContent),
|
||||
skillDirectory,
|
||||
CollectSourceSkillFiles(skillDirectory, skillFilePath));
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateSkillSourceRoots(string projectRoot)
|
||||
{
|
||||
HashSet<string> seenRoots = new(StringComparer.Ordinal);
|
||||
|
||||
AddSkillSourceRoot(seenRoots, GetCliOnlySkillSourceRoot(projectRoot));
|
||||
AddSkillSourceRoot(seenRoots, Path.Combine(projectRoot, "Assets"));
|
||||
foreach (string packageRoot in EnumerateDirectProjectPackageRoots(projectRoot))
|
||||
{
|
||||
AddSkillSourceRoot(seenRoots, packageRoot);
|
||||
}
|
||||
|
||||
foreach (string packageRoot in EnumerateManifestLocalPackageRoots(projectRoot))
|
||||
{
|
||||
AddSkillSourceRoot(seenRoots, packageRoot);
|
||||
}
|
||||
|
||||
foreach (string packageRoot in EnumerateDependencyPackageCacheRoots(projectRoot))
|
||||
{
|
||||
AddSkillSourceRoot(seenRoots, packageRoot);
|
||||
}
|
||||
|
||||
foreach (string root in seenRoots)
|
||||
{
|
||||
yield return root;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddSkillSourceRoot(HashSet<string> roots, string root)
|
||||
{
|
||||
if (string.IsNullOrEmpty(root))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
roots.Add(Path.GetFullPath(root));
|
||||
}
|
||||
|
||||
private static string GetCliOnlySkillSourceRoot(string projectRoot)
|
||||
{
|
||||
string currentProjectRoot = UnityMcpPathResolver.GetProjectRoot();
|
||||
if (!string.Equals(
|
||||
Path.GetFullPath(projectRoot),
|
||||
Path.GetFullPath(currentProjectRoot),
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Path.Combine(
|
||||
McpConstants.PackageResolvedPath,
|
||||
CliPackageDirName,
|
||||
McpConstants.SRC_DIR,
|
||||
SkillsDirName,
|
||||
CliSkillDefinitionsDirName,
|
||||
CliOnlySkillDefinitionsDirName);
|
||||
}
|
||||
|
||||
private static bool IsCliOnlySkillSourceRoot(string searchRoot)
|
||||
{
|
||||
return string.Equals(
|
||||
Path.GetFullPath(searchRoot),
|
||||
Path.GetFullPath(GetCliOnlySkillSourceRoot(UnityMcpPathResolver.GetProjectRoot())),
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateDirectProjectPackageRoots(string projectRoot)
|
||||
{
|
||||
string packagesRoot = Path.Combine(projectRoot, "Packages");
|
||||
if (!Directory.Exists(packagesRoot))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (string packageDirectory in Directory.EnumerateDirectories(packagesRoot))
|
||||
{
|
||||
yield return ResolveSkillSearchRootCandidate(packageDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateManifestLocalPackageRoots(string projectRoot)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> dependency in EnumerateManifestDependencies(projectRoot))
|
||||
{
|
||||
string localPath = ResolveLocalDependencyPath(dependency.Value, projectRoot);
|
||||
if (string.IsNullOrEmpty(localPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return ResolveSkillSearchRootCandidate(localPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateDependencyPackageCacheRoots(string projectRoot)
|
||||
{
|
||||
HashSet<string> dependencyNames = new(
|
||||
EnumerateManifestDependencies(projectRoot).Select(dependency => dependency.Key),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (dependencyNames.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
string packageCacheRoot = Path.Combine(projectRoot, "Library", "PackageCache");
|
||||
if (!Directory.Exists(packageCacheRoot))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (string packageDirectory in Directory.EnumerateDirectories(packageCacheRoot))
|
||||
{
|
||||
string packageName = Path.GetFileName(packageDirectory);
|
||||
if (string.IsNullOrEmpty(packageName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int separatorIndex = packageName.IndexOf('@');
|
||||
string dependencyName = separatorIndex >= 0 ? packageName.Substring(0, separatorIndex) : packageName;
|
||||
if (!dependencyNames.Contains(dependencyName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return ResolveSkillSearchRootCandidate(packageDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string>> EnumerateManifestDependencies(string projectRoot)
|
||||
{
|
||||
string manifestPath = Path.Combine(projectRoot, "Packages", "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
string manifestContent = File.ReadAllText(manifestPath);
|
||||
Match dependenciesMatch = Regex.Match(
|
||||
manifestContent,
|
||||
"\"dependencies\"\\s*:\\s*\\{(?<body>[\\s\\S]*?)\\}",
|
||||
RegexOptions.Multiline);
|
||||
if (!dependenciesMatch.Success)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
MatchCollection dependencyMatches = Regex.Matches(
|
||||
dependenciesMatch.Groups["body"].Value,
|
||||
"\"(?<name>[^\"]+)\"\\s*:\\s*\"(?<value>[^\"]*)\"");
|
||||
foreach (Match dependencyMatch in dependencyMatches)
|
||||
{
|
||||
string dependencyName = dependencyMatch.Groups["name"].Value;
|
||||
string dependencyValue = dependencyMatch.Groups["value"].Value;
|
||||
if (string.IsNullOrEmpty(dependencyName) || string.IsNullOrEmpty(dependencyValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new KeyValuePair<string, string>(dependencyName, dependencyValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLocalDependencyPath(string dependencyValue, string projectRoot)
|
||||
{
|
||||
const string FilePrefix = "file:";
|
||||
const string PathPrefix = "path:";
|
||||
|
||||
if (dependencyValue.StartsWith(FilePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return ResolveDependencyPath(dependencyValue.Substring(FilePrefix.Length), projectRoot);
|
||||
}
|
||||
|
||||
if (dependencyValue.StartsWith(PathPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return ResolveDependencyPath(dependencyValue.Substring(PathPrefix.Length), projectRoot);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ResolveDependencyPath(string rawPath, string projectRoot)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string normalizedPath = rawPath.Trim();
|
||||
if (normalizedPath.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
normalizedPath = normalizedPath.Substring(2);
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(normalizedPath))
|
||||
{
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(projectRoot, normalizedPath));
|
||||
}
|
||||
|
||||
private static string ResolveSkillSearchRootCandidate(string candidate)
|
||||
{
|
||||
string nestedRoot = Path.Combine(candidate, "Packages", "src");
|
||||
if (Directory.Exists(nestedRoot))
|
||||
{
|
||||
return nestedRoot;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateEditorFolders(string basePath, int maxDepth)
|
||||
{
|
||||
return EnumerateEditorFoldersRecursive(basePath, depth: 0, maxDepth);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateEditorFoldersRecursive(
|
||||
string currentPath,
|
||||
int depth,
|
||||
int maxDepth)
|
||||
{
|
||||
if (depth > maxDepth || !Directory.Exists(currentPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (string directory in Directory.EnumerateDirectories(currentPath))
|
||||
{
|
||||
string directoryName = Path.GetFileName(directory);
|
||||
if (string.Equals(directoryName, "Editor", StringComparison.Ordinal))
|
||||
{
|
||||
yield return directory;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string editorDirectory in EnumerateEditorFoldersRecursive(directory, depth + 1, maxDepth))
|
||||
{
|
||||
yield return editorDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseToolNameFromFrontmatter(string content)
|
||||
{
|
||||
Match frontmatterMatch = Regex.Match(content, @"^---\r?\n([\s\S]*?)\r?\n---");
|
||||
if (!frontmatterMatch.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string frontmatter = frontmatterMatch.Groups[1].Value;
|
||||
Match toolNameMatch = Regex.Match(frontmatter, @"^toolName:\s*(.+)$", RegexOptions.Multiline);
|
||||
if (!toolNameMatch.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return toolNameMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
private static string ParseNameFromFrontmatter(string content)
|
||||
{
|
||||
Match frontmatterMatch = Regex.Match(content, @"^---\r?\n([\s\S]*?)\r?\n---");
|
||||
if (!frontmatterMatch.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string frontmatter = frontmatterMatch.Groups[1].Value;
|
||||
Match nameMatch = Regex.Match(frontmatter, @"^name:\s*(.+)$", RegexOptions.Multiline);
|
||||
if (!nameMatch.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return nameMatch.Groups[1].Value.Trim().Trim('"');
|
||||
}
|
||||
|
||||
private static bool IsInternalSkill(string content)
|
||||
{
|
||||
Match frontmatterMatch = Regex.Match(content, @"^---\r?\n([\s\S]*?)\r?\n---");
|
||||
if (!frontmatterMatch.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string frontmatter = frontmatterMatch.Groups[1].Value;
|
||||
Match internalMatch = Regex.Match(frontmatter, @"^internal:\s*(.+)$", RegexOptions.Multiline);
|
||||
if (!internalMatch.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
internalMatch.Groups[1].Value.Trim(),
|
||||
"true",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsExcludedFile(string fileName)
|
||||
{
|
||||
if (ExcludedFileNames.Contains(fileName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (string excludedPattern in ExcludedFileNames)
|
||||
{
|
||||
if (fileName.EndsWith(excludedPattern, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSafeSkillPathComponent(string skillName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skillName == "." || skillName == "..")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skillName.Contains('/') || skillName.Contains('\\'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(skillName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skillName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Path.GetFileName(skillName), skillName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user