1import { Controller, Get, Param, Res } from "@nestjs/common";
2import * as fs from "node:fs";
3import * as path from "node:path";
4import { createRequire } from "node:module";
5import type { Response } from "express";
6import { parse as parseYaml } from "yaml";
7
8const req = createRequire(process.cwd());
9
10
32
33
39function repoRoot(): string {
40 if (fs.existsSync(path.join(process.cwd(), "docs", "api", "openapi.yaml"))) return process.cwd();
41 if (typeof __dirname !== "undefined") {
42
43 const up = path.resolve(__dirname, "..", "..", "..", "..");
44 if (fs.existsSync(path.join(up, "docs", "api", "openapi.yaml"))) return up;
45 }
46 let dir = process.cwd();
47 for (let i = 0; i < 6; i++) {
48 const parent = path.dirname(dir);
49 if (parent === dir) break;
50 dir = parent;
51 if (fs.existsSync(path.join(dir, "docs", "api", "openapi.yaml"))) return dir;
52 }
53 return process.cwd();
54}
55
56function specPath(): string {
57 return path.join(repoRoot(), "docs", "api", "openapi.yaml");
58}
59
60
61const ASSET_WHITELIST: Record<string, string> = {
62 "swagger-ui.css": "text/css; charset=utf-8",
63 "index.css": "text/css; charset=utf-8",
64 "swagger-ui-bundle.js": "application/javascript; charset=utf-8",
65 "swagger-ui-standalone-preset.js": "application/javascript; charset=utf-8",
66 "swagger-initializer.js": "application/javascript; charset=utf-8",
67 "favicon-16x16.png": "image/png",
68 "favicon-32x32.png": "image/png"
69};
70
71let swaggerDistDir: string | null = null;
72function distDir(): string {
73 if (!swaggerDistDir) {
74 const entry = req.resolve("swagger-ui-dist");
75 swaggerDistDir = path.dirname(entry);
76 }
77 return swaggerDistDir;
78}
79
80function readSpecYaml(): string {
81 return fs.readFileSync(specPath(), "utf8");
82}
83
84@Controller("docs")
85export class DocsController {
86
87 @Get()
88 index(@Res() res: Response): void {
89 const specJson = JSON.stringify(parseYaml(readSpecYaml())).replace(/</g, "\\u003c");
90 res.setHeader("Content-Type", "text/html; charset=utf-8");
91 res.setHeader("Cache-Control", "no-cache");
92 res.send(`<!doctype html>
93<html lang="en">
94<head>
95 <meta charset="utf-8"/>
96 <meta name="viewport" content="width=device-width, initial-scale=1"/>
97 <title>AI Software Factory — API v1</title>
98 <link rel="icon" type="image/png" href="/api/v1/docs/assets/favicon-32x32.png"/>
99 <link rel="stylesheet" href="/api/v1/docs/assets/swagger-ui.css"/>
100</head>
101<body>
102 <div id="app"></div>
103 <script id="openapi-spec" type="application/json">${specJson}</script>
104 <script src="/api/v1/docs/assets/swagger-ui-bundle.js"></script>
105 <script src="/api/v1/docs/assets/swagger-ui-standalone-preset.js"></script>
106 <script>
107 (function () {
108 var spec = JSON.parse(document.getElementById("openapi-spec").textContent);
109 window.ui = SwaggerUIBundle({
110 spec: spec,
111 dom_id: "#app",
112 deepLinking: true,
113 presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
114 layout: "StandaloneLayout",
115 requestInterceptor: function (req) {
116 // Docs demo: send NOTHING by default (the "Authorize" panel fills
117 // in the Bearer token for interactive calls).
118 return req;
119 }
120 });
121 })();
122 </script>
123</body>
124</html>`);
125 }
126
127
128 @Get("openapi.yaml")
129 spec(@Res() res: Response): void {
130 res.setHeader("Content-Type", "application/yaml; charset=utf-8");
131 res.setHeader("Cache-Control", "no-cache");
132 res.send(readSpecYaml());
133 }
134
135
136 @Get("assets/:file")
137 asset(@Param("file") file: string, @Res() res: Response): void {
138 const contentType = ASSET_WHITELIST[file];
139 if (!contentType) {
140 res.status(404).send("Not found");
141 return;
142 }
143 const full = path.join(distDir(), file);
144 if (!fs.existsSync(full)) {
145 res.status(404).send("Not found");
146 return;
147 }
148 res.setHeader("Content-Type", contentType);
149 res.setHeader("Cache-Control", "public, max-age=86400");
150 res.send(fs.readFileSync(full));
151 }
152}
153