import type { TerraformCategory, TerraformIndex, TerraformResource } from "./terraform-types"; import type { InternalResource, InternalUrl } from "./types"; export interface TerraformResolver { resolve(url: InternalUrl): Promise; } export function createTerraformResolver(index: TerraformIndex): TerraformResolver { const resources = normalizeResources(index.resources); const categoryBySlug = new Map(); for (const cat of index.categories) { categoryBySlug.set(cat.slug, cat); } const resourceToCategory = new Map(); for (const cat of index.categories) { for (const resName of cat.resources) { resourceToCategory.set(resName, cat.slug); } } return { async resolve(url: InternalUrl): Promise { const pathname = (url.rawPathname ?? url.pathname).replace(/^\/+/, "").replace(/\/+$/, ""); if (!pathname) { return makeResource(url, renderL0(index)); } const parts = pathname.split("/"); if (parts.length === 1) { const slug = parts[0]!; const cat = categoryBySlug.get(slug); if (cat) return makeResource(url, renderL1(cat, resources)); const res = resources[slug]; if (res) { const catSlug = resourceToCategory.get(slug) ?? ""; return makeResource(url, renderL2(slug, res, catSlug)); } return makeResource(url, renderUnknown(slug, resources)); } if (parts.length === 2) { const resourceName = parts[1]!; const res = resources[resourceName]; if (res) { const realCategory = resourceToCategory.get(resourceName) ?? parts[0]!; return makeResource(url, renderL2(resourceName, res, realCategory)); } return makeResource(url, renderUnknown(resourceName, resources)); } return makeResource(url, renderUnknown(pathname, resources)); }, }; } function normalizeResources(resources: TerraformIndex["resources"]): Readonly> { return Object.fromEntries(resources.map(resource => [resource.name, resource])); } function makeResource(url: InternalUrl, content: string): InternalResource { return { url: url.href, content, contentType: "text/markdown", size: Buffer.byteLength(content, "utf-8"), sourcePath: `xcsh://terraform${url.rawPathname ?? "/"}`, }; } function renderL0(index: TerraformIndex): string { const lines = [ `# F5 Distributed Cloud Terraform Provider (v${index.version})`, "", `> Source: \`${index.provider.source}\` | Registry: ${index.provider.registry}`, "", "## Provider Configuration (REQUIRED)", "", 'Every generated `.tf` MUST contain BOTH the `terraform {}` block and the `provider "xcsh" {}` block below.', 'Omitting the provider block causes `terraform plan` to fail with "Provider requires explicit configuration. Add a provider block".', "", "```hcl", `${index.provider.required_block}\n\n${index.provider.config_block}`, "```", "", "## Authentication", "", ...index.provider.auth_methods.map(m => `- ${m}`), "", "## Syntax Rules", "", ...index.provider.syntax_rules.map(r => `- ${r}`), "", "## Resource Categories", "", `${index.categories.length} categories. Read \`xcsh://terraform/{slug}\` for resource list.`, "", "| Category | Resources | Description |", "|----------|-----------|-------------|", ...index.categories.map(c => `| ${c.name} | ${c.resource_count} | ${c.description} |`), "", "## Quick Reference", "", "Common resources — output these in ```terraform code blocks:", "", "- `xcsh_http_loadbalancer`: name, namespace, domains, advertise_on_public_default_vip {}", '- `xcsh_origin_pool`: name, namespace, port, origin_servers { public_ip { ip = "x.x.x.x" } }', '- `xcsh_healthcheck`: name, namespace, http_health_check { path = "/healthz" }, timeout, interval, unhealthy_threshold, healthy_threshold', "- `xcsh_app_firewall`: name, namespace, blocking {}", "- `xcsh_service_policy`: name, namespace, rule_list { rules { ... } }, any_server {}", "- `xcsh_certificate`: name, namespace, certificate_url, private_key { blindfold_secret_info { location } }", "- `xcsh_rate_limiter_policy`: name, namespace, any_server {}", '- `xcsh_api_definition`: name, namespace, swagger_specs = ["string:///..."]', "- `xcsh_namespace`: name", "", "Import: `terraform import xcsh_{type}.example namespace/name`", 'Cross-refs use blocks: `app_firewall { name = "example-firewall" namespace = "example-namespace" }` not string refs.', "", ]; return lines.join("\n"); } function renderL1(cat: TerraformCategory, allResources: Readonly>): string { const lines = [ `# ${cat.name}`, "", cat.description, "", "## Resources", "", `Read \`xcsh://terraform/${cat.slug}/{resource}\` for full details.`, "", ]; for (const name of cat.resources) { const res = allResources[name]; const desc = res ? truncateAt(res.description, 80) : ""; lines.push(`- **${name}** — ${desc}`); } if (cat.dependency_chain) { lines.push("", "## Dependency Chain", "", cat.dependency_chain); } lines.push(""); return lines.join("\n"); } function renderL2(name: string, res: TerraformResource, _categorySlug: string): string { const lines = [`# xcsh_${name}`, "", res.description, "", `Required: ${res.required.join(", ") || "none"}`]; if (res.oneof_groups && res.oneof_groups.length > 0) { const groups = res.oneof_groups.filter(g => !g.parent).map(g => g.fields.join(" | ")); if (groups.length > 0) { lines.push("", "OneOf (pick one per group, use empty block `field {}`):"); for (const g of groups) lines.push(`- ${g}`); } } if (res.minimal_config) { lines.push("", "## Config", ""); lines.push(`Configuration source (${res.minimal_config.format}): \`${res.minimal_config.source}\``); } if (res.import_syntax) lines.push("", `Import: \`${res.import_syntax}\``); if (res.dependencies.requires.length > 0) { lines.push(`Depends on: ${res.dependencies.requires.join(", ")}`); } lines.push(""); return lines.join("\n"); } function renderUnknown(query: string, resources: Readonly>): string { const allNames = Object.keys(resources); const matches = allNames.filter(n => n.includes(query)).slice(0, 5); const suggestion = matches.length > 0 ? `\nDid you mean: ${matches.join(", ")}` : "\nUse `xcsh://terraform/` to list categories."; return `# Not found: ${query}\n${suggestion}\n`; } function truncateAt(s: string, maxLen: number): string { if (s.length <= maxLen) return s; const cut = s.lastIndexOf(" ", maxLen - 3); return `${s.slice(0, cut > 0 ? cut : maxLen - 3)}...`; }