@brainfish-ai/devdoc 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +415 -0
  3. package/bin/devdoc.js +13 -0
  4. package/dist/cli/commands/build.d.ts +5 -0
  5. package/dist/cli/commands/build.js +87 -0
  6. package/dist/cli/commands/check.d.ts +1 -0
  7. package/dist/cli/commands/check.js +143 -0
  8. package/dist/cli/commands/create.d.ts +24 -0
  9. package/dist/cli/commands/create.js +387 -0
  10. package/dist/cli/commands/deploy.d.ts +9 -0
  11. package/dist/cli/commands/deploy.js +433 -0
  12. package/dist/cli/commands/dev.d.ts +6 -0
  13. package/dist/cli/commands/dev.js +139 -0
  14. package/dist/cli/commands/init.d.ts +11 -0
  15. package/dist/cli/commands/init.js +238 -0
  16. package/dist/cli/commands/keys.d.ts +12 -0
  17. package/dist/cli/commands/keys.js +165 -0
  18. package/dist/cli/commands/start.d.ts +5 -0
  19. package/dist/cli/commands/start.js +56 -0
  20. package/dist/cli/commands/upload.d.ts +13 -0
  21. package/dist/cli/commands/upload.js +238 -0
  22. package/dist/cli/commands/whoami.d.ts +8 -0
  23. package/dist/cli/commands/whoami.js +91 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +106 -0
  26. package/dist/config/index.d.ts +80 -0
  27. package/dist/config/index.js +133 -0
  28. package/dist/constants.d.ts +9 -0
  29. package/dist/constants.js +13 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +12 -0
  32. package/dist/utils/logger.d.ts +16 -0
  33. package/dist/utils/logger.js +61 -0
  34. package/dist/utils/paths.d.ts +16 -0
  35. package/dist/utils/paths.js +50 -0
  36. package/package.json +51 -0
  37. package/renderer/app/api/assets/[...path]/route.ts +123 -0
  38. package/renderer/app/api/assets/route.ts +124 -0
  39. package/renderer/app/api/assets/upload/route.ts +177 -0
  40. package/renderer/app/api/auth-schemes/route.ts +77 -0
  41. package/renderer/app/api/chat/route.ts +858 -0
  42. package/renderer/app/api/codegen/route.ts +72 -0
  43. package/renderer/app/api/collections/route.ts +1016 -0
  44. package/renderer/app/api/debug/route.ts +53 -0
  45. package/renderer/app/api/deploy/route.ts +234 -0
  46. package/renderer/app/api/device/route.ts +42 -0
  47. package/renderer/app/api/docs/route.ts +187 -0
  48. package/renderer/app/api/keys/regenerate/route.ts +80 -0
  49. package/renderer/app/api/openapi-spec/route.ts +151 -0
  50. package/renderer/app/api/projects/[slug]/route.ts +153 -0
  51. package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
  52. package/renderer/app/api/projects/register/route.ts +152 -0
  53. package/renderer/app/api/proxy/route.ts +149 -0
  54. package/renderer/app/api/proxy-stream/route.ts +168 -0
  55. package/renderer/app/api/redirects/route.ts +47 -0
  56. package/renderer/app/api/schema/route.ts +65 -0
  57. package/renderer/app/api/subdomains/check/route.ts +172 -0
  58. package/renderer/app/api/suggestions/route.ts +144 -0
  59. package/renderer/app/favicon.ico +0 -0
  60. package/renderer/app/globals.css +1103 -0
  61. package/renderer/app/layout.tsx +47 -0
  62. package/renderer/app/llms-full.txt/route.ts +346 -0
  63. package/renderer/app/llms.txt/route.ts +279 -0
  64. package/renderer/app/page.tsx +14 -0
  65. package/renderer/app/robots.txt/route.ts +84 -0
  66. package/renderer/app/sitemap.xml/route.ts +199 -0
  67. package/renderer/components/docs/index.ts +12 -0
  68. package/renderer/components/docs/mdx/accordion.tsx +169 -0
  69. package/renderer/components/docs/mdx/badge.tsx +132 -0
  70. package/renderer/components/docs/mdx/callouts.tsx +154 -0
  71. package/renderer/components/docs/mdx/cards.tsx +213 -0
  72. package/renderer/components/docs/mdx/changelog.tsx +120 -0
  73. package/renderer/components/docs/mdx/code-block.tsx +186 -0
  74. package/renderer/components/docs/mdx/code-group.tsx +421 -0
  75. package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
  76. package/renderer/components/docs/mdx/frame.tsx +112 -0
  77. package/renderer/components/docs/mdx/highlight.tsx +151 -0
  78. package/renderer/components/docs/mdx/iframe.tsx +134 -0
  79. package/renderer/components/docs/mdx/image.tsx +235 -0
  80. package/renderer/components/docs/mdx/index.ts +204 -0
  81. package/renderer/components/docs/mdx/mermaid.tsx +240 -0
  82. package/renderer/components/docs/mdx/param-field.tsx +200 -0
  83. package/renderer/components/docs/mdx/steps.tsx +113 -0
  84. package/renderer/components/docs/mdx/tabs.tsx +86 -0
  85. package/renderer/components/docs/mdx-renderer.tsx +100 -0
  86. package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
  87. package/renderer/components/docs/navigation/index.ts +8 -0
  88. package/renderer/components/docs/navigation/page-nav.tsx +64 -0
  89. package/renderer/components/docs/navigation/sidebar.tsx +515 -0
  90. package/renderer/components/docs/navigation/toc.tsx +113 -0
  91. package/renderer/components/docs/notice.tsx +105 -0
  92. package/renderer/components/docs-header.tsx +274 -0
  93. package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
  94. package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
  95. package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
  96. package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
  97. package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
  98. package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
  99. package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
  100. package/renderer/components/docs-viewer/agent/index.tsx +6 -0
  101. package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
  102. package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
  103. package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
  104. package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
  105. package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
  106. package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
  107. package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
  108. package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
  109. package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
  110. package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
  111. package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
  112. package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
  113. package/renderer/components/docs-viewer/content/index.tsx +29 -0
  114. package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
  115. package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
  116. package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
  117. package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
  118. package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
  119. package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
  120. package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
  121. package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
  122. package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
  123. package/renderer/components/docs-viewer/index.tsx +1466 -0
  124. package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
  125. package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
  126. package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
  127. package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
  128. package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
  129. package/renderer/components/docs-viewer/playground/index.tsx +671 -0
  130. package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
  131. package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
  132. package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
  133. package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
  134. package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
  135. package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
  136. package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
  137. package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
  138. package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
  139. package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
  140. package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
  141. package/renderer/components/docs-viewer/search/index.ts +2 -0
  142. package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
  143. package/renderer/components/docs-viewer/search/use-search.ts +117 -0
  144. package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
  145. package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
  146. package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
  147. package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
  148. package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
  149. package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
  150. package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
  151. package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
  152. package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
  153. package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
  154. package/renderer/components/theme-provider.tsx +11 -0
  155. package/renderer/components/theme-toggle.tsx +76 -0
  156. package/renderer/components/ui/badge.tsx +46 -0
  157. package/renderer/components/ui/button.tsx +59 -0
  158. package/renderer/components/ui/dialog.tsx +118 -0
  159. package/renderer/components/ui/dropdown-menu.tsx +257 -0
  160. package/renderer/components/ui/input.tsx +21 -0
  161. package/renderer/components/ui/label.tsx +24 -0
  162. package/renderer/components/ui/navigation-menu.tsx +168 -0
  163. package/renderer/components/ui/select.tsx +190 -0
  164. package/renderer/components/ui/spinner.tsx +114 -0
  165. package/renderer/components/ui/tabs.tsx +66 -0
  166. package/renderer/components/ui/tooltip.tsx +61 -0
  167. package/renderer/hooks/use-code-copy.ts +88 -0
  168. package/renderer/hooks/use-openapi-title.ts +44 -0
  169. package/renderer/lib/api-docs/agent/index.ts +6 -0
  170. package/renderer/lib/api-docs/agent/indexer.ts +323 -0
  171. package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
  172. package/renderer/lib/api-docs/agent/types.ts +116 -0
  173. package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
  174. package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
  175. package/renderer/lib/api-docs/auth/crypto.ts +89 -0
  176. package/renderer/lib/api-docs/auth/index.ts +4 -0
  177. package/renderer/lib/api-docs/code-editor/db.ts +164 -0
  178. package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
  179. package/renderer/lib/api-docs/code-editor/index.ts +6 -0
  180. package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
  181. package/renderer/lib/api-docs/code-editor/types.ts +105 -0
  182. package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
  183. package/renderer/lib/api-docs/codegen/har.ts +251 -0
  184. package/renderer/lib/api-docs/codegen/index.ts +159 -0
  185. package/renderer/lib/api-docs/factories.ts +151 -0
  186. package/renderer/lib/api-docs/index.ts +17 -0
  187. package/renderer/lib/api-docs/mobile-context.tsx +112 -0
  188. package/renderer/lib/api-docs/navigation-context.tsx +88 -0
  189. package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
  190. package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
  191. package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
  192. package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
  193. package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
  194. package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
  195. package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
  196. package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
  197. package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
  198. package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
  199. package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
  200. package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
  201. package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
  202. package/renderer/lib/api-docs/playground/context.tsx +107 -0
  203. package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
  204. package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
  205. package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
  206. package/renderer/lib/api-docs/playground/types.ts +35 -0
  207. package/renderer/lib/api-docs/types.ts +269 -0
  208. package/renderer/lib/api-docs/utils.ts +311 -0
  209. package/renderer/lib/cache.ts +193 -0
  210. package/renderer/lib/docs/config/index.ts +29 -0
  211. package/renderer/lib/docs/config/loader.ts +142 -0
  212. package/renderer/lib/docs/config/schema.ts +298 -0
  213. package/renderer/lib/docs/index.ts +12 -0
  214. package/renderer/lib/docs/mdx/compiler.ts +176 -0
  215. package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
  216. package/renderer/lib/docs/mdx/index.ts +26 -0
  217. package/renderer/lib/docs/navigation/generator.ts +348 -0
  218. package/renderer/lib/docs/navigation/index.ts +12 -0
  219. package/renderer/lib/docs/navigation/types.ts +123 -0
  220. package/renderer/lib/docs-navigation-context.tsx +80 -0
  221. package/renderer/lib/multi-tenant/context.ts +105 -0
  222. package/renderer/lib/storage/blob.ts +845 -0
  223. package/renderer/lib/utils.ts +6 -0
  224. package/renderer/next.config.ts +76 -0
  225. package/renderer/package.json +66 -0
  226. package/renderer/postcss.config.mjs +5 -0
  227. package/renderer/public/assets/images/screenshot.png +0 -0
  228. package/renderer/public/assets/logo/dark.svg +9 -0
  229. package/renderer/public/assets/logo/light.svg +9 -0
  230. package/renderer/public/assets/logo.svg +9 -0
  231. package/renderer/public/file.svg +1 -0
  232. package/renderer/public/globe.svg +1 -0
  233. package/renderer/public/icon.png +0 -0
  234. package/renderer/public/logo.svg +9 -0
  235. package/renderer/public/window.svg +1 -0
  236. package/renderer/tsconfig.json +28 -0
  237. package/templates/basic/README.md +139 -0
  238. package/templates/basic/assets/favicon.svg +4 -0
  239. package/templates/basic/assets/logo.svg +9 -0
  240. package/templates/basic/docs.json +47 -0
  241. package/templates/basic/guides/configuration.mdx +149 -0
  242. package/templates/basic/guides/overview.mdx +96 -0
  243. package/templates/basic/index.mdx +39 -0
  244. package/templates/basic/package.json +14 -0
  245. package/templates/basic/quickstart.mdx +92 -0
  246. package/templates/basic/vercel.json +6 -0
  247. package/templates/graphql/README.md +139 -0
  248. package/templates/graphql/api-reference/schema.graphql +305 -0
  249. package/templates/graphql/assets/favicon.svg +4 -0
  250. package/templates/graphql/assets/logo.svg +9 -0
  251. package/templates/graphql/docs.json +54 -0
  252. package/templates/graphql/guides/configuration.mdx +149 -0
  253. package/templates/graphql/guides/overview.mdx +96 -0
  254. package/templates/graphql/index.mdx +39 -0
  255. package/templates/graphql/package.json +14 -0
  256. package/templates/graphql/quickstart.mdx +92 -0
  257. package/templates/graphql/vercel.json +6 -0
  258. package/templates/openapi/README.md +139 -0
  259. package/templates/openapi/api-reference/openapi.json +419 -0
  260. package/templates/openapi/assets/favicon.svg +4 -0
  261. package/templates/openapi/assets/logo.svg +9 -0
  262. package/templates/openapi/docs.json +61 -0
  263. package/templates/openapi/guides/configuration.mdx +149 -0
  264. package/templates/openapi/guides/overview.mdx +96 -0
  265. package/templates/openapi/index.mdx +39 -0
  266. package/templates/openapi/package.json +14 -0
  267. package/templates/openapi/quickstart.mdx +92 -0
  268. package/templates/openapi/vercel.json +6 -0
@@ -0,0 +1,1016 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readFileSync, existsSync, readdirSync } from 'fs'
3
+ import { join, basename, isAbsolute } from 'path'
4
+ import matter from 'gray-matter'
5
+ import { CacheUtils } from '@/lib/cache'
6
+ import { importOpenAPISpec } from '@/lib/api-docs/parsers/openapi'
7
+ import { importGraphQLSchema, type BrainfishGraphQLCollection } from '@/lib/api-docs/parsers/graphql'
8
+ import { generateAPISummary, formatAPISummaryForPrompt } from '@/lib/api-docs/agent/spec-summary'
9
+ import { getProjectContent, type ProjectContent } from '@/lib/storage/blob'
10
+ import type { OpenAPI } from 'openapi-types'
11
+ import type { BrainfishCollection, BrainfishDocGroup, BrainfishDocPage } from '@/lib/api-docs/types'
12
+
13
+ // Configuration
14
+ const USE_LOCAL_SPEC = process.env.USE_LOCAL_SPEC !== 'false' // Default to true
15
+ const STARTER_PATH = process.env.STARTER_PATH || 'devdoc-docs'
16
+ const LOCAL_SPEC_PATH = process.env.LOCAL_SPEC_PATH || `${STARTER_PATH}/api-reference/openapi.json`
17
+
18
+ // Helper to resolve paths - supports both relative and absolute STARTER_PATH
19
+ function resolvePath(...paths: string[]): string {
20
+ if (isAbsolute(STARTER_PATH)) {
21
+ return join(STARTER_PATH, ...paths)
22
+ }
23
+ return join(process.cwd(), STARTER_PATH, ...paths)
24
+ }
25
+
26
+ // Get the content root directory
27
+ function getContentRoot(): string {
28
+ if (isAbsolute(STARTER_PATH)) {
29
+ return STARTER_PATH
30
+ }
31
+ return join(process.cwd(), STARTER_PATH)
32
+ }
33
+ const BRAINFISH_API_BASE_URL = process.env.BRAINFISH_API_BASE_URL || 'https://api.brainfish.ai/api'
34
+ const BRAINFISH_CATALOG_ID = process.env.BRAINFISH_CATALOG_ID || 'your_catalog_id_here'
35
+ const BRAINFISH_JWT_TOKEN = process.env.BRAINFISH_JWT_TOKEN || 'your_jwt_token_here'
36
+
37
+ // Debug logging
38
+ if (process.env.NODE_ENV === 'development') {
39
+ console.log('[Collections] Config:', {
40
+ useLocalSpec: USE_LOCAL_SPEC,
41
+ starterPath: STARTER_PATH,
42
+ localSpecPath: LOCAL_SPEC_PATH,
43
+ })
44
+ }
45
+
46
+ // Cache configuration
47
+ const CACHE_KEY = USE_LOCAL_SPEC ? `collections-local` : `collections-${BRAINFISH_CATALOG_ID}`
48
+ const CACHE_TTL = 60 * 5 // 5 minutes
49
+ const FALLBACK_CACHE_KEY = `${CACHE_KEY}-fallback`
50
+ const SUMMARY_CACHE_KEY = USE_LOCAL_SPEC ? `api-summary-local` : `api-summary-${BRAINFISH_CATALOG_ID}`
51
+ const SUMMARY_CACHE_TTL = 60 * 60 * 24 // 1 day
52
+
53
+ interface OpenApiSpec {
54
+ openapi: string
55
+ info: {
56
+ title: string
57
+ version: string
58
+ description: string
59
+ }
60
+ paths: Record<string, unknown>
61
+ servers?: Array<{
62
+ url: string
63
+ description: string
64
+ }>
65
+ }
66
+
67
+ // API version configuration
68
+ interface ApiVersionConfig {
69
+ version: string
70
+ spec: string // Path to the OpenAPI spec file
71
+ default?: boolean
72
+ }
73
+
74
+ // GraphQL schema configuration
75
+ interface GraphQLSchemaConfig {
76
+ name?: string
77
+ schema: string // Path to the GraphQL schema file (.graphql or .gql)
78
+ endpoint: string // GraphQL endpoint URL
79
+ default?: boolean
80
+ }
81
+
82
+ // Nested group within pages array
83
+ interface DocsConfigNestedGroup {
84
+ group: string
85
+ pages: string[]
86
+ }
87
+
88
+ // Page can be a string path or a nested group
89
+ type DocsConfigPageItem = string | DocsConfigNestedGroup
90
+
91
+ interface DocsConfigTab {
92
+ tab: string
93
+ type?: 'docs' | 'openapi' | 'changelog' | 'graphql'
94
+ path?: string // URL path for the tab (e.g., "/api-reference", "/changelog")
95
+ versions?: ApiVersionConfig[] // Multiple API versions (for openapi type)
96
+ // GraphQL-specific config
97
+ schema?: string // Path to single GraphQL schema file
98
+ endpoint?: string // GraphQL endpoint URL
99
+ schemas?: GraphQLSchemaConfig[] // Multiple GraphQL schemas
100
+ groups?: Array<{
101
+ group: string
102
+ pages: DocsConfigPageItem[]
103
+ icon?: string // Phosphor icon name for the group header
104
+ }>
105
+ }
106
+
107
+ interface DocsConfigLogo {
108
+ url?: string
109
+ alt?: string
110
+ width?: number
111
+ height?: number
112
+ light?: string
113
+ dark?: string
114
+ }
115
+
116
+ interface DocsConfigHeader {
117
+ showAskAI?: boolean
118
+ showSearch?: boolean
119
+ showThemeToggle?: boolean
120
+ }
121
+
122
+ interface DocsConfigNavbarLink {
123
+ label: string
124
+ href: string
125
+ external?: boolean
126
+ }
127
+
128
+ interface DocsConfigNavbarPrimary {
129
+ label: string
130
+ href: string
131
+ }
132
+
133
+ interface DocsConfigNavbar {
134
+ links?: DocsConfigNavbarLink[]
135
+ primary?: DocsConfigNavbarPrimary
136
+ }
137
+
138
+ interface DocsConfigNotice {
139
+ content: string
140
+ dismissible?: boolean
141
+ background?: string
142
+ }
143
+
144
+ interface DocsConfigRedirect {
145
+ source: string
146
+ destination: string
147
+ }
148
+
149
+ interface DocsConfig {
150
+ name?: string
151
+ favicon?: string
152
+ notice?: DocsConfigNotice
153
+ redirects?: DocsConfigRedirect[]
154
+ navigation?: {
155
+ tabs?: DocsConfigTab[]
156
+ }
157
+ }
158
+
159
+ // Theme configuration (from theme.json)
160
+ interface ThemeConfig {
161
+ logo?: DocsConfigLogo
162
+ header?: DocsConfigHeader
163
+ navbar?: DocsConfigNavbar
164
+ colors?: {
165
+ primary?: string
166
+ primaryLight?: string
167
+ primaryDark?: string
168
+ }
169
+ typography?: {
170
+ fontFamily?: string
171
+ fontFamilyMono?: string
172
+ baseFontSize?: string
173
+ }
174
+ defaultTheme?: 'light' | 'dark' | 'system'
175
+ customCss?: string
176
+ }
177
+
178
+ // API version for the response
179
+ interface ApiVersion {
180
+ version: string
181
+ spec: string
182
+ default: boolean
183
+ }
184
+
185
+ // GraphQL schema for the response
186
+ interface GraphQLSchemaInfo {
187
+ name: string
188
+ schema: string
189
+ endpoint: string
190
+ default: boolean
191
+ }
192
+
193
+ // Navigation tab type for the response
194
+ interface NavigationTab {
195
+ id: string
196
+ tab: string // Tab name (used for both navigation and page header)
197
+ type: 'docs' | 'openapi' | 'changelog' | 'graphql'
198
+ path?: string
199
+ order: number
200
+ versions?: ApiVersion[] // Available API versions (for openapi type)
201
+ graphqlSchemas?: GraphQLSchemaInfo[] // Available GraphQL schemas (for graphql type)
202
+ }
203
+
204
+ // Changelog release type
205
+ interface ChangelogRelease {
206
+ version: string
207
+ date: string
208
+ title: string
209
+ slug: string
210
+ }
211
+
212
+ // Load GraphQL schema from local file
213
+ function loadGraphQLSchema(schemaPath: string): string | null {
214
+ try {
215
+ // Resolve the path similar to OpenAPI spec
216
+ const relativePath = schemaPath
217
+ const fullPath = relativePath.startsWith(STARTER_PATH) || isAbsolute(relativePath)
218
+ ? (isAbsolute(relativePath) ? relativePath : resolvePath(relativePath.replace(STARTER_PATH + '/', '')))
219
+ : resolvePath(relativePath)
220
+
221
+ if (!existsSync(fullPath)) {
222
+ console.log('[Collections] GraphQL schema not found at:', fullPath)
223
+ return null
224
+ }
225
+
226
+ const content = readFileSync(fullPath, 'utf-8')
227
+ console.log('[Collections] Loaded GraphQL schema from:', fullPath)
228
+ return content
229
+ } catch (error) {
230
+ console.error('[Collections] Error loading GraphQL schema:', error)
231
+ return null
232
+ }
233
+ }
234
+
235
+ // Load OpenAPI spec from local file
236
+ function loadLocalSpec(specPath?: string): OpenApiSpec | null {
237
+ try {
238
+ // Use provided path or default
239
+ const relativePath = specPath || LOCAL_SPEC_PATH
240
+ // If the path already includes STARTER_PATH prefix, use it; otherwise prepend
241
+ const fullPath = relativePath.startsWith(STARTER_PATH) || isAbsolute(relativePath)
242
+ ? (isAbsolute(relativePath) ? relativePath : resolvePath(relativePath.replace(STARTER_PATH + '/', '')))
243
+ : resolvePath(relativePath)
244
+
245
+ if (!existsSync(fullPath)) {
246
+ console.log('[Collections] Local spec not found at:', fullPath)
247
+ return null
248
+ }
249
+
250
+ const content = readFileSync(fullPath, 'utf-8')
251
+ const spec = JSON.parse(content) as OpenApiSpec
252
+
253
+ console.log('[Collections] Loaded local spec:', spec.info?.title, 'v' + spec.info?.version, 'from', fullPath)
254
+ return spec
255
+ } catch (error) {
256
+ console.error('[Collections] Error loading local spec:', error)
257
+ return null
258
+ }
259
+ }
260
+
261
+ // Load docs.json configuration
262
+ function loadDocsConfig(): DocsConfig | null {
263
+ try {
264
+ const configPath = resolvePath('docs.json')
265
+
266
+ if (!existsSync(configPath)) {
267
+ console.log('[Collections] docs.json not found at:', configPath)
268
+ return null
269
+ }
270
+
271
+ const content = readFileSync(configPath, 'utf-8')
272
+ return JSON.parse(content) as DocsConfig
273
+ } catch (error) {
274
+ console.error('[Collections] Error loading docs.json:', error)
275
+ return null
276
+ }
277
+ }
278
+
279
+ // Load theme.json configuration
280
+ function loadThemeConfig(): ThemeConfig | null {
281
+ try {
282
+ const configPath = resolvePath('theme.json')
283
+
284
+ if (!existsSync(configPath)) {
285
+ console.log('[Collections] theme.json not found at:', configPath)
286
+ return null
287
+ }
288
+
289
+ const content = readFileSync(configPath, 'utf-8')
290
+ return JSON.parse(content) as ThemeConfig
291
+ } catch (error) {
292
+ console.error('[Collections] Error loading theme.json:', error)
293
+ return null
294
+ }
295
+ }
296
+
297
+ // Load custom CSS content
298
+ function loadCustomCss(cssPath?: string): string | null {
299
+ if (!cssPath) return null
300
+
301
+ try {
302
+ const fullPath = resolvePath(cssPath)
303
+
304
+ if (!existsSync(fullPath)) {
305
+ return null
306
+ }
307
+
308
+ return readFileSync(fullPath, 'utf-8')
309
+ } catch (error) {
310
+ console.error('[Collections] Error loading custom CSS:', error)
311
+ return null
312
+ }
313
+ }
314
+
315
+ // Load MDX page metadata
316
+ function loadPageMeta(pagePath: string): { title: string; description?: string; icon?: string } | null {
317
+ try {
318
+ const contentRoot = getContentRoot()
319
+
320
+ // Try .mdx then .md
321
+ let fullPath = join(contentRoot, `${pagePath}.mdx`)
322
+ if (!existsSync(fullPath)) {
323
+ fullPath = join(contentRoot, `${pagePath}.md`)
324
+ }
325
+ if (!existsSync(fullPath)) {
326
+ return null
327
+ }
328
+
329
+ const content = readFileSync(fullPath, 'utf-8')
330
+ const { data } = matter(content)
331
+
332
+ return {
333
+ title: data.title || basename(pagePath),
334
+ description: data.description,
335
+ icon: data.icon,
336
+ }
337
+ } catch (error) {
338
+ console.error('[Collections] Error loading page meta:', pagePath, error)
339
+ return null
340
+ }
341
+ }
342
+
343
+ // Extract navigation tabs from docs.json in order
344
+ function buildNavigationTabs(config: DocsConfig): NavigationTab[] {
345
+ const tabs: NavigationTab[] = []
346
+
347
+ if (!config.navigation?.tabs) {
348
+ return tabs
349
+ }
350
+
351
+ config.navigation.tabs.forEach((tab, index) => {
352
+ // Convert tab name to id (e.g., "API Reference" -> "api-reference")
353
+ const id = tab.tab.toLowerCase().replace(/\s+/g, '-')
354
+
355
+ // Determine the type (default to 'docs' if not specified)
356
+ const type = tab.type || 'docs'
357
+
358
+ // Build versions array for openapi tabs
359
+ let versions: ApiVersion[] | undefined
360
+ if (tab.type === 'openapi' && tab.versions && tab.versions.length > 0) {
361
+ versions = tab.versions.map(v => ({
362
+ version: v.version,
363
+ spec: v.spec,
364
+ default: v.default || false,
365
+ }))
366
+ // Ensure at least one default
367
+ if (!versions.some(v => v.default)) {
368
+ versions[0].default = true
369
+ }
370
+ }
371
+
372
+ // Build graphql schemas array for graphql tabs
373
+ let graphqlSchemas: GraphQLSchemaInfo[] | undefined
374
+ if (tab.type === 'graphql') {
375
+ if (tab.schemas && tab.schemas.length > 0) {
376
+ // Multiple schemas
377
+ graphqlSchemas = tab.schemas.map(s => ({
378
+ name: s.name || 'GraphQL API',
379
+ schema: s.schema,
380
+ endpoint: s.endpoint,
381
+ default: s.default || false,
382
+ }))
383
+ // Ensure at least one default
384
+ if (!graphqlSchemas.some(s => s.default)) {
385
+ graphqlSchemas[0].default = true
386
+ }
387
+ } else if (tab.schema && tab.endpoint) {
388
+ // Single schema
389
+ graphqlSchemas = [{
390
+ name: tab.tab,
391
+ schema: tab.schema,
392
+ endpoint: tab.endpoint,
393
+ default: true,
394
+ }]
395
+ }
396
+ }
397
+
398
+ tabs.push({
399
+ id,
400
+ tab: tab.tab,
401
+ type,
402
+ path: tab.path,
403
+ order: index,
404
+ versions,
405
+ graphqlSchemas,
406
+ })
407
+ })
408
+
409
+ return tabs
410
+ }
411
+
412
+ // Build documentation groups from docs.json
413
+ function buildDocGroups(config: DocsConfig): BrainfishDocGroup[] {
414
+ const groups: BrainfishDocGroup[] = []
415
+
416
+ if (!config.navigation?.tabs) {
417
+ return groups
418
+ }
419
+
420
+ // Helper to process a page item (string or nested group)
421
+ const processPageItem = (
422
+ pageItem: DocsConfigPageItem,
423
+ index: number,
424
+ parentGroup: string
425
+ ): BrainfishDocPage | null => {
426
+ // Handle nested group
427
+ if (typeof pageItem === 'object' && 'group' in pageItem) {
428
+ const nestedGroup = pageItem as DocsConfigNestedGroup
429
+ const children: BrainfishDocPage[] = []
430
+
431
+ for (let j = 0; j < nestedGroup.pages.length; j++) {
432
+ const childPath = nestedGroup.pages[j]
433
+ if (typeof childPath === 'string') {
434
+ const childMeta = loadPageMeta(childPath)
435
+ if (childMeta) {
436
+ children.push({
437
+ id: `doc-${childPath.replace(/\//g, '-')}`,
438
+ slug: childPath,
439
+ title: childMeta.title,
440
+ description: childMeta.description,
441
+ icon: childMeta.icon,
442
+ filePath: childPath,
443
+ group: nestedGroup.group,
444
+ order: j,
445
+ })
446
+ }
447
+ }
448
+ }
449
+
450
+ if (children.length > 0) {
451
+ // Return a group item with children
452
+ return {
453
+ id: `group-${nestedGroup.group}`.toLowerCase().replace(/\s+/g, '-'),
454
+ slug: '', // Groups don't have a slug themselves
455
+ title: nestedGroup.group,
456
+ filePath: '',
457
+ group: parentGroup,
458
+ order: index,
459
+ isGroup: true,
460
+ children,
461
+ }
462
+ }
463
+ return null
464
+ }
465
+
466
+ // Handle string page path
467
+ const pagePath = pageItem as string
468
+ const meta = loadPageMeta(pagePath)
469
+
470
+ if (meta) {
471
+ return {
472
+ id: `doc-${pagePath.replace(/\//g, '-')}`,
473
+ slug: pagePath,
474
+ title: meta.title,
475
+ description: meta.description,
476
+ icon: meta.icon,
477
+ filePath: pagePath,
478
+ group: parentGroup,
479
+ order: index,
480
+ }
481
+ }
482
+ return null
483
+ }
484
+
485
+ // Iterate through all tabs and their groups
486
+ for (const tab of config.navigation.tabs) {
487
+ // Skip tabs without groups (like API Reference with just href)
488
+ if (!tab.groups) {
489
+ continue
490
+ }
491
+
492
+ for (const group of tab.groups) {
493
+ const pages: BrainfishDocPage[] = []
494
+
495
+ for (let i = 0; i < group.pages.length; i++) {
496
+ const pageItem = group.pages[i]
497
+ const page = processPageItem(pageItem, i, group.group)
498
+ if (page) {
499
+ pages.push(page)
500
+ }
501
+ }
502
+
503
+ if (pages.length > 0) {
504
+ groups.push({
505
+ id: `group-${tab.tab}-${group.group}`.toLowerCase().replace(/\s+/g, '-'),
506
+ title: group.group,
507
+ pages,
508
+ order: groups.length,
509
+ icon: group.icon,
510
+ })
511
+ }
512
+ }
513
+ }
514
+
515
+ return groups
516
+ }
517
+
518
+ // Scan changelog directory for release files
519
+ function scanChangelogReleases(): ChangelogRelease[] {
520
+ const releases: ChangelogRelease[] = []
521
+ const changelogDir = resolvePath('changelog')
522
+
523
+ if (!existsSync(changelogDir)) {
524
+ return releases
525
+ }
526
+
527
+ try {
528
+ const files = readdirSync(changelogDir)
529
+
530
+ for (const file of files) {
531
+ // Only process versioned files (v1.0.0.mdx, etc.), skip latest.mdx and index files
532
+ if (!file.match(/^v[\d.]+\.mdx?$/)) {
533
+ continue
534
+ }
535
+
536
+ const filePath = join(changelogDir, file)
537
+ const content = readFileSync(filePath, 'utf-8')
538
+ const { data } = matter(content)
539
+
540
+ const version = data.version || file.replace(/\.mdx?$/, '')
541
+
542
+ releases.push({
543
+ version,
544
+ date: data.date || '',
545
+ title: data.title || version,
546
+ slug: `changelog/${file.replace(/\.mdx?$/, '')}`,
547
+ })
548
+ }
549
+
550
+ // Sort by version (newest first)
551
+ releases.sort((a, b) => {
552
+ const versionA = a.version.replace(/^v/, '').split('.').map(Number)
553
+ const versionB = b.version.replace(/^v/, '').split('.').map(Number)
554
+
555
+ for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
556
+ const numA = versionA[i] || 0
557
+ const numB = versionB[i] || 0
558
+ if (numA !== numB) return numB - numA
559
+ }
560
+ return 0
561
+ })
562
+
563
+ } catch (error) {
564
+ console.error('[Collections] Error scanning changelog:', error)
565
+ }
566
+
567
+ return releases
568
+ }
569
+
570
+ // Fetch OpenAPI spec from Brainfish API
571
+ async function fetchRemoteSpec(): Promise<OpenApiSpec | null> {
572
+ const cachedSpec = await CacheUtils.get<OpenApiSpec>(CACHE_KEY)
573
+ if (cachedSpec) {
574
+ return cachedSpec
575
+ }
576
+
577
+ try {
578
+ const url = `${BRAINFISH_API_BASE_URL}/catalogs.openapi-spec`
579
+
580
+ console.log('[Collections] Fetching from:', url)
581
+
582
+ const response = await fetch(url, {
583
+ method: 'POST',
584
+ headers: {
585
+ 'Authorization': `Bearer ${BRAINFISH_JWT_TOKEN}`,
586
+ 'Accept': 'application/json',
587
+ 'Content-Type': 'application/json',
588
+ 'User-Agent': 'Brainfish-API-Docs/1.0',
589
+ },
590
+ body: JSON.stringify({ catalogId: BRAINFISH_CATALOG_ID }),
591
+ cache: 'no-store',
592
+ })
593
+
594
+ if (!response.ok) {
595
+ let errorBody = ''
596
+ try {
597
+ errorBody = await response.text()
598
+ } catch {
599
+ // ignore
600
+ }
601
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ''}`)
602
+ }
603
+
604
+ const spec = await response.json() as OpenApiSpec
605
+
606
+ await CacheUtils.set(CACHE_KEY, spec, CACHE_TTL)
607
+ await CacheUtils.set(FALLBACK_CACHE_KEY, spec, 60 * 60 * 24 * 365)
608
+
609
+ return spec
610
+ } catch (error) {
611
+ console.error('[Collections] Error fetching remote spec:', error)
612
+
613
+ const fallbackSpec = await CacheUtils.get<OpenApiSpec>(FALLBACK_CACHE_KEY)
614
+ if (fallbackSpec) {
615
+ console.warn('[Collections] Using fallback cache')
616
+ return fallbackSpec
617
+ }
618
+
619
+ return null
620
+ }
621
+ }
622
+
623
+ // Get OpenAPI spec (local or remote)
624
+ async function getOpenApiSpec(specPath?: string): Promise<OpenApiSpec | null> {
625
+ if (USE_LOCAL_SPEC) {
626
+ const localSpec = loadLocalSpec(specPath)
627
+ if (localSpec) {
628
+ return localSpec
629
+ }
630
+ console.log('[Collections] Local spec not available, falling back to remote')
631
+ }
632
+
633
+ return fetchRemoteSpec()
634
+ }
635
+
636
+ /**
637
+ * Handle multi-tenant request - fetch content from Blob Storage
638
+ */
639
+ async function handleMultiTenantRequest(projectSlug: string, request: Request): Promise<Response> {
640
+ try {
641
+ const projectContent = await getProjectContent(projectSlug)
642
+
643
+ if (!projectContent) {
644
+ return NextResponse.json(
645
+ { error: 'Project not found' },
646
+ { status: 404 }
647
+ )
648
+ }
649
+
650
+ // Parse docs.json from project content
651
+ const docsConfig = JSON.parse(projectContent.docsJson) as DocsConfig
652
+
653
+ // Build navigation tabs and doc groups from config
654
+ const navigationTabs = buildNavigationTabs(docsConfig)
655
+ const docGroups = buildDocGroupsFromBlob(docsConfig, projectContent)
656
+
657
+ // For now, return docs-only response (no OpenAPI spec from blob)
658
+ return NextResponse.json(
659
+ {
660
+ id: projectSlug,
661
+ name: docsConfig.name || 'Documentation',
662
+ description: 'Documentation',
663
+ folders: [],
664
+ requests: [],
665
+ auth: { authType: 'none', authActive: true },
666
+ headers: [],
667
+ variables: [],
668
+ apiSummary: null,
669
+ docGroups,
670
+ navigationTabs,
671
+ changelogReleases: [],
672
+ docsName: docsConfig.name || null,
673
+ docsFavicon: docsConfig.favicon || null,
674
+ docsLogo: null,
675
+ docsHeader: null,
676
+ docsNavbar: null,
677
+ docsColors: null,
678
+ defaultTheme: null,
679
+ customCss: null,
680
+ apiVersions: [],
681
+ selectedApiVersion: null,
682
+ notice: docsConfig.notice || null,
683
+ isMultiTenant: true,
684
+ projectSlug,
685
+ },
686
+ {
687
+ headers: {
688
+ 'Content-Type': 'application/json',
689
+ 'Cache-Control': 'public, max-age=60',
690
+ },
691
+ }
692
+ )
693
+ } catch (error) {
694
+ console.error('[Collections] Multi-tenant error:', error)
695
+ return NextResponse.json(
696
+ { error: 'Failed to load project' },
697
+ { status: 500 }
698
+ )
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Build doc groups from Blob storage content
704
+ */
705
+ function buildDocGroupsFromBlob(config: DocsConfig, content: ProjectContent): BrainfishDocGroup[] {
706
+ const groups: BrainfishDocGroup[] = []
707
+
708
+ if (!config.navigation?.tabs) {
709
+ return groups
710
+ }
711
+
712
+ // Helper to find file content
713
+ const findFileContent = (pagePath: string): { title: string; description?: string; icon?: string } | null => {
714
+ const mdxPath = `${pagePath}.mdx`
715
+ const mdPath = `${pagePath}.md`
716
+
717
+ const file = content.files.find(f => f.path === mdxPath || f.path === mdPath)
718
+ if (!file) return null
719
+
720
+ try {
721
+ const { data } = matter(file.content)
722
+ return {
723
+ title: data.title || basename(pagePath),
724
+ description: data.description,
725
+ icon: data.icon,
726
+ }
727
+ } catch {
728
+ return { title: basename(pagePath) }
729
+ }
730
+ }
731
+
732
+ // Process tabs and groups
733
+ for (const tab of config.navigation.tabs) {
734
+ if (!tab.groups) continue
735
+
736
+ for (const group of tab.groups) {
737
+ const pages: BrainfishDocPage[] = []
738
+
739
+ for (let i = 0; i < group.pages.length; i++) {
740
+ const pageItem = group.pages[i]
741
+
742
+ if (typeof pageItem === 'string') {
743
+ const meta = findFileContent(pageItem)
744
+ if (meta) {
745
+ pages.push({
746
+ id: `doc-${pageItem.replace(/\//g, '-')}`,
747
+ slug: pageItem,
748
+ title: meta.title,
749
+ description: meta.description,
750
+ icon: meta.icon,
751
+ filePath: pageItem,
752
+ group: group.group,
753
+ order: i,
754
+ })
755
+ }
756
+ } else if ('group' in pageItem) {
757
+ // Nested group
758
+ const children: BrainfishDocPage[] = []
759
+ for (let j = 0; j < pageItem.pages.length; j++) {
760
+ const childPath = pageItem.pages[j]
761
+ if (typeof childPath === 'string') {
762
+ const childMeta = findFileContent(childPath)
763
+ if (childMeta) {
764
+ children.push({
765
+ id: `doc-${childPath.replace(/\//g, '-')}`,
766
+ slug: childPath,
767
+ title: childMeta.title,
768
+ description: childMeta.description,
769
+ icon: childMeta.icon,
770
+ filePath: childPath,
771
+ group: pageItem.group,
772
+ order: j,
773
+ })
774
+ }
775
+ }
776
+ }
777
+
778
+ if (children.length > 0) {
779
+ pages.push({
780
+ id: `group-${pageItem.group}`.toLowerCase().replace(/\s+/g, '-'),
781
+ slug: '',
782
+ title: pageItem.group,
783
+ filePath: '',
784
+ group: group.group,
785
+ order: i,
786
+ isGroup: true,
787
+ children,
788
+ })
789
+ }
790
+ }
791
+ }
792
+
793
+ if (pages.length > 0) {
794
+ groups.push({
795
+ id: `group-${tab.tab}-${group.group}`.toLowerCase().replace(/\s+/g, '-'),
796
+ title: group.group,
797
+ pages,
798
+ order: groups.length,
799
+ icon: group.icon,
800
+ })
801
+ }
802
+ }
803
+ }
804
+
805
+ return groups
806
+ }
807
+
808
+ // Helper to generate or get cached summary
809
+ async function getOrGenerateSummary(collection: BrainfishCollection, specVersion: string): Promise<string> {
810
+ const versionedCacheKey = `${SUMMARY_CACHE_KEY}-v${specVersion}`
811
+
812
+ const cachedSummary = await CacheUtils.get<string>(versionedCacheKey)
813
+ if (cachedSummary) {
814
+ console.log('[Collections] Using cached API summary for version:', specVersion)
815
+ return cachedSummary
816
+ }
817
+
818
+ console.log('[Collections] Generating new API summary for version:', specVersion)
819
+ const summary = generateAPISummary(collection)
820
+ const formattedSummary = formatAPISummaryForPrompt(summary)
821
+
822
+ await CacheUtils.set(versionedCacheKey, formattedSummary, SUMMARY_CACHE_TTL)
823
+
824
+ return formattedSummary
825
+ }
826
+
827
+ export async function GET(request: Request) {
828
+ try {
829
+ // Check for multi-tenant mode (project slug from middleware)
830
+ const projectSlug = request.headers.get('x-devdoc-project')
831
+
832
+ // If multi-tenant, fetch from Blob Storage
833
+ if (projectSlug && !projectSlug.startsWith('custom:')) {
834
+ return handleMultiTenantRequest(projectSlug, request)
835
+ }
836
+
837
+ // Parse query params
838
+ const { searchParams } = new URL(request.url)
839
+ const requestedVersion = searchParams.get('version')
840
+
841
+ // Load docs and theme configuration
842
+ const docsConfig = loadDocsConfig()
843
+ const themeConfig = loadThemeConfig()
844
+ const customCss = loadCustomCss(themeConfig?.customCss)
845
+ const docGroups = docsConfig ? buildDocGroups(docsConfig) : []
846
+ const navigationTabs = docsConfig ? buildNavigationTabs(docsConfig) : []
847
+ const changelogReleases = scanChangelogReleases()
848
+
849
+ // Find the OpenAPI tab and its versions
850
+ const openapiTab = navigationTabs.find(t => t.type === 'openapi')
851
+ const apiVersions = openapiTab?.versions || []
852
+
853
+ // Determine which spec to load
854
+ let specPath: string | undefined
855
+ let selectedVersion: string | undefined
856
+
857
+ if (apiVersions.length > 0) {
858
+ // Find requested version or default
859
+ const versionConfig = requestedVersion
860
+ ? apiVersions.find(v => v.version === requestedVersion)
861
+ : apiVersions.find(v => v.default) || apiVersions[0]
862
+
863
+ if (versionConfig) {
864
+ specPath = versionConfig.spec
865
+ selectedVersion = versionConfig.version
866
+ }
867
+ }
868
+
869
+ // Get OpenAPI spec (with optional path for versioned specs)
870
+ const spec = await getOpenApiSpec(specPath)
871
+
872
+ console.log('[Collections] Loaded', docGroups.length, 'doc groups,', navigationTabs.length, 'tabs,', changelogReleases.length, 'changelog releases,', apiVersions.length, 'API versions, selected:', selectedVersion)
873
+
874
+ // Handle no spec available
875
+ if (!spec) {
876
+ return NextResponse.json(
877
+ {
878
+ id: 'empty',
879
+ name: docsConfig?.name || 'Documentation',
880
+ description: 'Welcome to the documentation.',
881
+ folders: [],
882
+ requests: [],
883
+ auth: { authType: 'none', authActive: true },
884
+ headers: [],
885
+ variables: [],
886
+ apiSummary: null,
887
+ docGroups,
888
+ navigationTabs,
889
+ changelogReleases,
890
+ docsName: docsConfig?.name || null,
891
+ docsFavicon: docsConfig?.favicon || null,
892
+ docsLogo: themeConfig?.logo || null,
893
+ docsHeader: themeConfig?.header || null,
894
+ docsNavbar: themeConfig?.navbar || null,
895
+ docsColors: themeConfig?.colors || null,
896
+ defaultTheme: themeConfig?.defaultTheme || null,
897
+ customCss: customCss || null,
898
+ apiVersions,
899
+ selectedApiVersion: selectedVersion,
900
+ notice: docsConfig?.notice || null,
901
+ },
902
+ {
903
+ headers: {
904
+ 'Content-Type': 'application/json',
905
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
906
+ },
907
+ }
908
+ )
909
+ }
910
+
911
+ // Check if spec has paths
912
+ if (!spec.paths || Object.keys(spec.paths).length === 0) {
913
+ return NextResponse.json(
914
+ {
915
+ id: 'empty',
916
+ name: spec.info?.title || docsConfig?.name || 'Documentation',
917
+ description: spec.info?.description || 'No endpoints defined.',
918
+ folders: [],
919
+ requests: [],
920
+ auth: { authType: 'none', authActive: true },
921
+ headers: [],
922
+ variables: [],
923
+ apiSummary: null,
924
+ docGroups,
925
+ navigationTabs,
926
+ changelogReleases,
927
+ docsName: docsConfig?.name || null,
928
+ docsFavicon: docsConfig?.favicon || null,
929
+ docsLogo: themeConfig?.logo || null,
930
+ docsHeader: themeConfig?.header || null,
931
+ docsNavbar: themeConfig?.navbar || null,
932
+ docsColors: themeConfig?.colors || null,
933
+ defaultTheme: themeConfig?.defaultTheme || null,
934
+ customCss: customCss || null,
935
+ apiVersions,
936
+ selectedApiVersion: selectedVersion,
937
+ notice: docsConfig?.notice || null,
938
+ },
939
+ {
940
+ headers: {
941
+ 'Content-Type': 'application/json',
942
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
943
+ },
944
+ }
945
+ )
946
+ }
947
+
948
+ // Convert to BrainfishCollection using our parser
949
+ const collections = await importOpenAPISpec(spec as OpenAPI.Document)
950
+ const collection = collections[0]
951
+
952
+ if (!collection) {
953
+ return NextResponse.json(null, {
954
+ headers: {
955
+ 'Content-Type': 'application/json',
956
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
957
+ },
958
+ })
959
+ }
960
+
961
+ // Generate or get cached API summary
962
+ const specVersion = spec.info?.version || 'unknown'
963
+ const apiSummary = await getOrGenerateSummary(collection, specVersion)
964
+
965
+ // Return collection with summary and doc groups
966
+ return NextResponse.json(
967
+ {
968
+ ...collection,
969
+ apiSummary,
970
+ specVersion,
971
+ docGroups,
972
+ navigationTabs,
973
+ changelogReleases,
974
+ docsName: docsConfig?.name || null,
975
+ docsFavicon: docsConfig?.favicon || null,
976
+ docsLogo: themeConfig?.logo || null,
977
+ docsHeader: themeConfig?.header || null,
978
+ docsNavbar: themeConfig?.navbar || null,
979
+ docsColors: themeConfig?.colors || null,
980
+ defaultTheme: themeConfig?.defaultTheme || null,
981
+ customCss: customCss || null,
982
+ apiVersions,
983
+ selectedApiVersion: selectedVersion,
984
+ notice: docsConfig?.notice || null,
985
+ },
986
+ {
987
+ headers: {
988
+ 'Content-Type': 'application/json',
989
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
990
+ },
991
+ }
992
+ )
993
+ } catch (error) {
994
+ console.error('[Collections] Unexpected error:', error)
995
+
996
+ return NextResponse.json(
997
+ {
998
+ id: 'error',
999
+ name: 'Documentation',
1000
+ description: 'Failed to load documentation.',
1001
+ folders: [],
1002
+ requests: [],
1003
+ auth: { authType: 'none', authActive: true },
1004
+ headers: [],
1005
+ variables: [],
1006
+ docGroups: [],
1007
+ },
1008
+ {
1009
+ headers: {
1010
+ 'Content-Type': 'application/json',
1011
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
1012
+ },
1013
+ }
1014
+ )
1015
+ }
1016
+ }