| 1 | using Tomlyn; |
| 2 | using Tomlyn.Model; |
| 3 | using AgentForge.Plugin.Sdk; |
| 4 | |
| 5 | namespace AgentForge.Plugin.RepositoryImport.GitHub.Core; |
| 6 | |
| 7 | public sealed class ForgeImportCatalogDocument |
| 8 | { |
| 9 | public ForgeImportCatalogSettings Catalog { get; set; } = new(); |
| 10 | |
| 11 | public List<ForgeImportCatalogExplicit> Explicit { get; set; } = []; |
| 12 | |
| 13 | public List<ForgeImportCatalogRule> Rules { get; set; } = []; |
| 14 | |
| 15 | public List<ForgeImportCatalogTopicRule> TopicRules { get; set; } = []; |
| 16 | } |
| 17 | |
| 18 | public sealed class ForgeImportCatalogSettings |
| 19 | { |
| 20 | public string ForgeOrg { get; set; } = ""; |
| 21 | |
| 22 | public string GitHubOrg { get; set; } = ""; |
| 23 | |
| 24 | public string UnmatchedCatalog { get; set; } = "backup/unclassified"; |
| 25 | |
| 26 | public string UnmatchedVisibility { get; set; } = "private"; |
| 27 | |
| 28 | public List<string> SkipRepos { get; set; } = []; |
| 29 | |
| 30 | public List<string> EnsureGroups { get; set; } = []; |
| 31 | } |
| 32 | |
| 33 | public sealed class ForgeImportCatalogExplicit |
| 34 | { |
| 35 | public string GitHubName { get; set; } = ""; |
| 36 | |
| 37 | public string? ForgeSlug { get; set; } |
| 38 | |
| 39 | public string CatalogPath { get; set; } = ""; |
| 40 | |
| 41 | public string? Visibility { get; set; } |
| 42 | } |
| 43 | |
| 44 | public sealed class ForgeImportCatalogRule |
| 45 | { |
| 46 | public string Id { get; set; } = ""; |
| 47 | |
| 48 | public string? NameExact { get; set; } |
| 49 | |
| 50 | public List<string> NameOneOf { get; set; } = []; |
| 51 | |
| 52 | public string? NamePrefix { get; set; } |
| 53 | |
| 54 | public string? NameSuffix { get; set; } |
| 55 | |
| 56 | public string? CatalogPath { get; set; } |
| 57 | |
| 58 | public string? CatalogPathTemplate { get; set; } |
| 59 | |
| 60 | public string? Visibility { get; set; } |
| 61 | } |
| 62 | |
| 63 | public sealed class ForgeImportCatalogTopicRule |
| 64 | { |
| 65 | public List<string> TopicsAll { get; set; } = []; |
| 66 | |
| 67 | public string CatalogPath { get; set; } = ""; |
| 68 | |
| 69 | public string? Visibility { get; set; } |
| 70 | } |
| 71 | |
| 72 | public sealed class ForgeImportCatalogLoader |
| 73 | { |
| 74 | public ForgeImportCatalogDocument Load(string path) |
| 75 | { |
| 76 | if (!File.Exists(path)) |
| 77 | throw new ForgeApiException(404, $"Import catalog config not found: '{path}'."); |
| 78 | |
| 79 | var model = Toml.ToModel(File.ReadAllText(path)); |
| 80 | var doc = new ForgeImportCatalogDocument(); |
| 81 | if (model.TryGetValue("catalog", out var catalogObj) && catalogObj is TomlTable catalogTable) |
| 82 | doc.Catalog = ReadSettings(catalogTable); |
| 83 | |
| 84 | ReadArraySection(model, "catalog", "explicit", table => doc.Explicit.Add(ReadExplicit(table))); |
| 85 | ReadArraySection(model, "catalog", "rules", table => doc.Rules.Add(ReadRule(table))); |
| 86 | ReadArraySection(model, "catalog", "topic_rules", table => doc.TopicRules.Add(ReadTopicRule(table))); |
| 87 | |
| 88 | return doc; |
| 89 | } |
| 90 | |
| 91 | public string ResolveDefaultPath(string contentRoot, string? forgeOrg) |
| 92 | { |
| 93 | var slug = string.IsNullOrWhiteSpace(forgeOrg) ? "ai-guiders" : forgeOrg.Trim(); |
| 94 | var candidates = new[] |
| 95 | { |
| 96 | Path.Combine(contentRoot, "config", $"import-catalog.{slug}.toml"), |
| 97 | Path.Combine(contentRoot, "config", "import-catalog.ai-guiders.toml"), |
| 98 | Path.Combine(AppContext.BaseDirectory, "config", $"import-catalog.{slug}.toml"), |
| 99 | Path.Combine(AppContext.BaseDirectory, "config", "import-catalog.ai-guiders.toml"), |
| 100 | }; |
| 101 | |
| 102 | return candidates.FirstOrDefault(File.Exists) ?? candidates[0]; |
| 103 | } |
| 104 | |
| 105 | private static void ReadArraySection( |
| 106 | TomlTable model, |
| 107 | string root, |
| 108 | string key, |
| 109 | Action<TomlTable> read) |
| 110 | { |
| 111 | if (!model.TryGetValue(root, out var rootObj) || rootObj is not TomlTable rootTable) |
| 112 | return; |
| 113 | if (!rootTable.TryGetValue(key, out var arrayObj) || arrayObj is not TomlTableArray array) |
| 114 | return; |
| 115 | foreach (var table in array) |
| 116 | read(table); |
| 117 | } |
| 118 | |
| 119 | private static ForgeImportCatalogSettings ReadSettings(TomlTable table) |
| 120 | { |
| 121 | var settings = new ForgeImportCatalogSettings(); |
| 122 | if (table.TryGetValue("forge_org", out var forgeObj) && forgeObj is string forge) |
| 123 | settings.ForgeOrg = forge.Trim(); |
| 124 | if (table.TryGetValue("github_org", out var ghObj) && ghObj is string gh) |
| 125 | settings.GitHubOrg = gh.Trim(); |
| 126 | if (table.TryGetValue("unmatched_catalog", out var unmatchedObj) && unmatchedObj is string unmatched) |
| 127 | settings.UnmatchedCatalog = unmatched.Trim(); |
| 128 | if (table.TryGetValue("unmatched_visibility", out var visObj) && visObj is string vis) |
| 129 | settings.UnmatchedVisibility = vis.Trim(); |
| 130 | settings.SkipRepos = ReadStringList(table, "skip_repos"); |
| 131 | settings.EnsureGroups = ReadStringList(table, "ensure_groups"); |
| 132 | return settings; |
| 133 | } |
| 134 | |
| 135 | private static ForgeImportCatalogExplicit ReadExplicit(TomlTable table) => new() |
| 136 | { |
| 137 | GitHubName = ReadString(table, "github_name") ?? "", |
| 138 | ForgeSlug = ReadString(table, "forge_slug"), |
| 139 | CatalogPath = ReadString(table, "catalog_path") ?? "", |
| 140 | Visibility = ReadString(table, "visibility"), |
| 141 | }; |
| 142 | |
| 143 | private static ForgeImportCatalogRule ReadRule(TomlTable table) => new() |
| 144 | { |
| 145 | Id = ReadString(table, "id") ?? "", |
| 146 | NameExact = ReadString(table, "name_exact"), |
| 147 | NameOneOf = ReadStringList(table, "name_one_of"), |
| 148 | NamePrefix = ReadString(table, "name_prefix"), |
| 149 | NameSuffix = ReadString(table, "name_suffix"), |
| 150 | CatalogPath = ReadString(table, "catalog_path"), |
| 151 | CatalogPathTemplate = ReadString(table, "catalog_path_template"), |
| 152 | Visibility = ReadString(table, "visibility"), |
| 153 | }; |
| 154 | |
| 155 | private static ForgeImportCatalogTopicRule ReadTopicRule(TomlTable table) => new() |
| 156 | { |
| 157 | TopicsAll = ReadStringList(table, "topics_all"), |
| 158 | CatalogPath = ReadString(table, "catalog_path") ?? "", |
| 159 | Visibility = ReadString(table, "visibility"), |
| 160 | }; |
| 161 | |
| 162 | private static string? ReadString(TomlTable table, string key) => |
| 163 | table.TryGetValue(key, out var obj) && obj is string value && !string.IsNullOrWhiteSpace(value) |
| 164 | ? value.Trim() |
| 165 | : null; |
| 166 | |
| 167 | private static List<string> ReadStringList(TomlTable table, string key) |
| 168 | { |
| 169 | if (!table.TryGetValue(key, out var obj)) |
| 170 | return []; |
| 171 | return obj switch |
| 172 | { |
| 173 | TomlArray array => array.OfType<string>().Select(s => s.Trim()).Where(s => s.Length > 0).ToList(), |
| 174 | string single when single.Length > 0 => [single.Trim()], |
| 175 | _ => [], |
| 176 | }; |
| 177 | } |
| 178 | } |
| 179 | |