import { reflectRoutes } from "@nifrajs/core/reflection" const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/ /** The subset of JSON Schema fields {@link tsTypeOf} reads to render a TypeScript type string. */ interface JsonSchemaNode { readonly type?: string readonly anyOf?: readonly unknown[] readonly oneOf?: readonly unknown[] readonly enum?: readonly unknown[] readonly const?: unknown readonly items?: unknown readonly properties?: Readonly> readonly required?: readonly string[] readonly additionalProperties?: unknown } function tsTypeOf(schema: unknown, depth = 0): string { if (typeof schema !== "object" || schema === null || depth > 6) return JSON.stringify(schema) const node = schema as JsonSchemaNode const union = node.anyOf ?? node.oneOf if (Array.isArray(union)) return union.map((u) => tsTypeOf(u, depth + 1)).join(" | ") if (Array.isArray(node.enum)) return node.enum.map((v) => JSON.stringify(v)).join(" | ") if (node.const !== undefined) return JSON.stringify(node.const) switch (node.type) { case "string": return "string" case "number": case "integer": return "number" case "boolean": return "boolean" case "null": return "null" case "array": { const item = tsTypeOf(node.items, depth + 1) return item.includes(" | ") ? `(${item})[]` : `${item}[]` } case "object": { const props = node.properties if (props === undefined) { return node.additionalProperties === undefined || node.additionalProperties === false ? "{}" : `Record` } const required = new Set(Array.isArray(node.required) ? (node.required as string[]) : []) const fields = Object.entries(props).map( ([key, value]) => `${key}${required.has(key) ? "" : "?"}: ${tsTypeOf(value, depth + 1)}`, ) return `{ ${fields.join(", ")} }` } default: return JSON.stringify(schema) } } function clientCall(method: string, path: string, schema: unknown): string { const s = schema as { body?: unknown; query?: unknown } | undefined const verb = method.toLowerCase() const segs = path.split("/").filter((seg) => seg !== "") let chain = "api" if (segs.length === 0) chain += ".index" else for (const seg of segs) { if (seg.startsWith(":") || seg.startsWith("*")) { const name = seg.replace(/^[:*]/, "") || "value" chain += `({ ${name} })` } else chain += IDENT.test(seg) ? `.${seg}` : `[${JSON.stringify(seg)}]` } const isBodyVerb = verb === "post" || verb === "put" || verb === "patch" let call: string if (isBodyVerb) { const bodyArg = s?.body ? "body" : s?.query ? "undefined" : "" const opts = s?.query ? (bodyArg ? ", { query }" : "{ query }") : "" call = `.${verb}(${bodyArg}${opts})` } else { call = `.${verb}(${s?.query ? "{ query }" : ""})` } return `await ${chain}${call}` } /** Read the project's `AGENTS.md`, or `""` if there isn't one (or we're on an edge runtime with no * filesystem). Never throws - a missing file just means no guidelines section. */ async function readAgentsMd(): Promise { // Edge bundles define this flag and dead-code-eliminate the filesystem branch. Keeping the import // behind a runtime-only `typeof Bun` check is insufficient: Rollup must still emit an unresolved // `node:` dependency, which workerd cannot load. const edgeRuntime = ( globalThis as typeof globalThis & { readonly __NIFRA_EDGE_RUNTIME__?: boolean } ).__NIFRA_EDGE_RUNTIME__ if (edgeRuntime === true) return "" try { if (typeof Bun !== "undefined") return await Bun.file("AGENTS.md").text() const fs = await import("node:fs/promises") return await fs.readFile("AGENTS.md", "utf-8") } catch { return "" } } export async function generateLlmsTxt( full: boolean, pageRoutes: ReadonlyArray<{ readonly pattern: string; readonly id: string }>, backend: unknown, options: { readonly includeLocalGuidelines?: boolean } = {}, ): Promise { let output = "" output += `# Nifra App Context\n\n` output += `This is a machine-readable context endpoint describing the API routes, pages, and conventions of this Nifra application.\n\n` // 1. Project Guidelines. Off unless the app opts in: `AGENTS.md` is a repo file written for the // team, not a page, and this endpoint is public - a note about an unshipped feature or an // internal hostname in it would be served to anyone who asks. const agentsMd = options.includeLocalGuidelines === true ? await readAgentsMd() : "" if (agentsMd) { output += `## Guidelines & Conventions\n\n` output += `${agentsMd}\n\n` } // 2. Page Routes output += `## Page Routes\n\n` for (const page of pageRoutes) { output += `- Page \`${page.pattern}\` (route ID: \`${page.id}\`)\n` } output += `\n` // 3. API Routes output += `## API Routes\n\n` const apiRoutes = reflectRoutes(backend) if (apiRoutes.length === 0) { output += `No API routes registered.\n` } else { for (const route of apiRoutes) { output += `- **${route.method}** \`${route.path}\`\n` if (full) { output += ` - Client Call: \`${clientCall(route.method, route.path, route.schema)}\`\n` const s = route.schema const bodySchema = s?.body?.jsonSchema const querySchema = s?.query?.jsonSchema const responseSchema = s?.response?.jsonSchema if (bodySchema !== undefined) { output += ` - Body Schema: \`${tsTypeOf(bodySchema)}\`\n` } if (querySchema !== undefined) { output += ` - Query Schema: \`${tsTypeOf(querySchema)}\`\n` } if (responseSchema !== undefined) { output += ` - Response Schema: \`${tsTypeOf(responseSchema)}\`\n` } if (s?.errors) { for (const [status, errorSchema] of Object.entries(s.errors)) { const errJson = errorSchema.jsonSchema if (errJson !== undefined) { output += ` - Error ${status} Schema: \`${tsTypeOf(errJson)}\`\n` } } } } } } return output }