@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.
- package/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- 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
|
+
}
|