| 1 | using System.IO; |
| 2 | using CascadeIDE.Features.Workspace; |
| 3 | |
| 4 | namespace CascadeIDE.Services; |
| 5 | |
| 6 | internal static class DocsTemplatesCatalogResolver |
| 7 | { |
| 8 | public sealed record TemplateEntry( |
| 9 | string Id, |
| 10 | string Title, |
| 11 | string Kind, |
| 12 | string Source, |
| 13 | string? RepoPath, |
| 14 | string? KnowledgeRootId, |
| 15 | string? KnowledgeFilePath); |
| 16 | |
| 17 | private sealed class DocsTemplatesCatalogRoot |
| 18 | { |
| 19 | public RepositoryDocsTemplatesToml? DocsTemplates { get; set; } |
| 20 | } |
| 21 | |
| 22 | public static IReadOnlyList<TemplateEntry> ResolveTemplatesFromWorkspaceToml( |
| 23 | RepositoryWorkspaceToml? workspaceToml, |
| 24 | string workspaceRoot) |
| 25 | { |
| 26 | var list = new List<TemplateEntry>(); |
| 27 | |
| 28 | // 1) catalog file |
| 29 | var catalogPath = workspaceToml?.Workspace?.DocsTemplates?.CatalogPath; |
| 30 | if (!string.IsNullOrWhiteSpace(catalogPath)) |
| 31 | { |
| 32 | var abs = WorkspaceAdrMapResolver.TryResolveAbsoluteDocPath(workspaceRoot, catalogPath!); |
| 33 | if (!string.IsNullOrWhiteSpace(abs) && File.Exists(abs)) |
| 34 | { |
| 35 | try |
| 36 | { |
| 37 | var raw = File.ReadAllText(abs); |
| 38 | var parsed = CascadeTomlSerializer.Deserialize<DocsTemplatesCatalogRoot>(raw); |
| 39 | if (parsed?.DocsTemplates?.Template is { Count: > 0 } templates) |
| 40 | Append(list, templates); |
| 41 | } |
| 42 | catch |
| 43 | { |
| 44 | // ignore |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // 2) inline |
| 50 | if (workspaceToml?.Workspace?.DocsTemplates?.Template is { Count: > 0 } inline) |
| 51 | Append(list, inline); |
| 52 | |
| 53 | // 3) fallback built-ins (repo) |
| 54 | if (list.Count == 0) |
| 55 | { |
| 56 | list.Add(new TemplateEntry("feature.v1", "Feature doc", "feature_doc", "repo", "docs/templates/feature.md", null, null)); |
| 57 | list.Add(new TemplateEntry("module.v1", "Module doc", "module_doc", "repo", "docs/templates/module.md", null, null)); |
| 58 | list.Add(new TemplateEntry("adr-mini.v1", "ADR mini", "adr", "repo", "docs/templates/adr-mini.md", null, null)); |
| 59 | list.Add(new TemplateEntry("runbook.v1", "Runbook", "runbook", "repo", "docs/templates/runbook.md", null, null)); |
| 60 | } |
| 61 | |
| 62 | // De-dup by id (later wins). |
| 63 | var dict = new Dictionary<string, TemplateEntry>(StringComparer.OrdinalIgnoreCase); |
| 64 | foreach (var t in list) |
| 65 | dict[t.Id] = t; |
| 66 | |
| 67 | return dict.Values.OrderBy(x => x.Id, StringComparer.Ordinal).ToList(); |
| 68 | } |
| 69 | |
| 70 | private static void Append(List<TemplateEntry> dst, List<DocsTemplateToml> templates) |
| 71 | { |
| 72 | foreach (var t in templates) |
| 73 | { |
| 74 | var id = (t.Id ?? "").Trim(); |
| 75 | if (id.Length == 0) |
| 76 | continue; |
| 77 | |
| 78 | var title = (t.Title ?? id).Trim(); |
| 79 | var kind = (t.Kind ?? "").Trim(); |
| 80 | var source = (t.Source ?? "repo").Trim().ToLowerInvariant(); |
| 81 | |
| 82 | if (source == "knowledge") |
| 83 | { |
| 84 | var rootId = (t.KnowledgeRootId ?? "").Trim(); |
| 85 | var file = (t.FilePath ?? "").Trim(); |
| 86 | if (file.Length == 0) |
| 87 | continue; |
| 88 | |
| 89 | dst.Add(new TemplateEntry( |
| 90 | id, |
| 91 | title, |
| 92 | kind, |
| 93 | "knowledge", |
| 94 | null, |
| 95 | rootId.Length == 0 ? null : rootId, |
| 96 | file)); |
| 97 | continue; |
| 98 | } |
| 99 | |
| 100 | var path = (t.Path ?? "").Trim(); |
| 101 | if (path.Length == 0) |
| 102 | continue; |
| 103 | |
| 104 | dst.Add(new TemplateEntry(id, title, kind, "repo", path, null, null)); |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | |