camelmind 0.2.1 → 0.2.3
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/dist/index.js +224 -6
- package/package.json +1 -1
- package/template/_package.json +4 -0
- package/template/app/[...slug]/page.tsx +1 -1
- package/template/app/api/llms/[...slug]/route.ts +34 -0
- package/template/app/api/raw/route.ts +34 -1
- package/template/app/api-reference/[[...slug]]/page.tsx +65 -22
- package/template/app/globals.css +1 -2
- package/template/app/llms.txt/route.ts +36 -0
- package/template/components/ApiReference/ApiSidebar.tsx +66 -5
- package/template/components/Nav/TopNav.tsx +1 -1
- package/template/components/Nav/VersionSelector.tsx +8 -0
- package/template/components/mdx/Callout.tsx +3 -1
- package/template/components/mdx/LLMIgnore.tsx +4 -0
- package/template/components/mdx/LLMOnly.tsx +4 -0
- package/template/components/mdx/index.tsx +4 -0
- package/template/lib/api-reference.ts +8 -9
- package/template/lib/config-types.ts +12 -1
- package/template/lib/config.ts +1 -1
- package/template/lib/mdx.ts +13 -0
- package/template/lib/nav.ts +72 -3
- package/template/lib/versions.ts +62 -26
- package/template/proxy.ts +10 -1
- package/template/scripts/build-offline.sh +43 -5
- package/template/scripts/build-pdf.sh +3 -3
- package/template/scripts/build-search-index.ts +7 -1
- package/template/scripts/generate-master-pdf.ts +9 -17
- package/template/scripts/generate-pdfs.ts +26 -12
- package/template/public/2f-logo.png +0 -0
|
@@ -132,7 +132,7 @@ export function TopNav({ nav, userRoles, userName, authEnabled = false, versions
|
|
|
132
132
|
currentVersionId={currentVersionId}
|
|
133
133
|
currentSlug={currentSlug}
|
|
134
134
|
versionSlugs={versionSlugs}
|
|
135
|
-
isLoggedIn={!!userName}
|
|
135
|
+
isLoggedIn={!!userName || !authEnabled}
|
|
136
136
|
/>
|
|
137
137
|
{authEnabled && (
|
|
138
138
|
userName ? (
|
|
@@ -21,6 +21,14 @@ export function VersionSelector({ versions, currentVersionId, currentSlug, versi
|
|
|
21
21
|
|
|
22
22
|
function navigateToVersion(targetId: string) {
|
|
23
23
|
setOpen(false)
|
|
24
|
+
|
|
25
|
+
// API reference pages use a different URL scheme (/api-reference/[version]/...)
|
|
26
|
+
// so navigate directly to the target version's API reference overview
|
|
27
|
+
if (currentSlug.startsWith("/api-reference")) {
|
|
28
|
+
router.push(`/api-reference/${targetId}`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
const bare = currentVersionId
|
|
25
33
|
? currentSlug.replace(new RegExp(`^/${currentVersionId}`), "") || "/"
|
|
26
34
|
: currentSlug
|
|
@@ -23,10 +23,12 @@ const defaultIcons: Record<CalloutType, string> = {
|
|
|
23
23
|
export function Callout({
|
|
24
24
|
type = "note",
|
|
25
25
|
icon,
|
|
26
|
+
title,
|
|
26
27
|
children,
|
|
27
28
|
}: {
|
|
28
29
|
type?: CalloutType
|
|
29
30
|
icon?: string
|
|
31
|
+
title?: string
|
|
30
32
|
children: React.ReactNode
|
|
31
33
|
}) {
|
|
32
34
|
const iconName = icon ?? defaultIcons[type]
|
|
@@ -35,7 +37,7 @@ export function Callout({
|
|
|
35
37
|
<div className={`callout ${type} my-4`}>
|
|
36
38
|
<div className="callout-header flex items-center gap-1.5">
|
|
37
39
|
<Icon name={iconName} size={13} />
|
|
38
|
-
{labels[type]}
|
|
40
|
+
{title ?? labels[type]}
|
|
39
41
|
</div>
|
|
40
42
|
<div className="callout-body">{children}</div>
|
|
41
43
|
</div>
|
|
@@ -3,6 +3,8 @@ import { Steps, Step } from "./Steps"
|
|
|
3
3
|
import { Tabs, Tab } from "./Tabs"
|
|
4
4
|
import { Icon } from "./Icon"
|
|
5
5
|
import { Details } from "./Details"
|
|
6
|
+
import { LLMOnly } from "./LLMOnly"
|
|
7
|
+
import { LLMIgnore } from "./LLMIgnore"
|
|
6
8
|
import { CodeBlock } from "@/components/Code/CodeBlock"
|
|
7
9
|
|
|
8
10
|
export const mdxComponents = {
|
|
@@ -13,6 +15,8 @@ export const mdxComponents = {
|
|
|
13
15
|
Tab,
|
|
14
16
|
Icon,
|
|
15
17
|
Details,
|
|
18
|
+
LLMOnly,
|
|
19
|
+
LLMIgnore,
|
|
16
20
|
pre: ({ children }: { children: React.ReactNode }) => {
|
|
17
21
|
// If the child code block has a language class, let CodeBlock handle it
|
|
18
22
|
const child = children as React.ReactElement<{ className?: string }>
|
|
@@ -15,22 +15,21 @@ import type {
|
|
|
15
15
|
HttpMethod,
|
|
16
16
|
} from "./api-types"
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
let _cachedLanguages: string | null = null
|
|
18
|
+
const _specCache = new Map<string, ParsedSpec>()
|
|
20
19
|
|
|
21
20
|
export function loadApiSpec(specPath: string, languages?: string[]): ParsedSpec {
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
const cacheKey = `${specPath}|${JSON.stringify(languages ?? null)}`
|
|
22
|
+
const cached = _specCache.get(cacheKey)
|
|
23
|
+
if (cached) return cached
|
|
24
24
|
const raw = fs.readFileSync(path.join(process.cwd(), specPath), "utf-8")
|
|
25
25
|
const doc = yaml.load(raw) as Record<string, unknown>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
26
|
+
const parsed = parseOpenApi(doc, languages)
|
|
27
|
+
_specCache.set(cacheKey, parsed)
|
|
28
|
+
return parsed
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function clearApiCache() {
|
|
32
|
-
|
|
33
|
-
_cachedLanguages = null
|
|
32
|
+
_specCache.clear()
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
function slugify(str: string): string {
|
|
@@ -22,14 +22,24 @@ export type SiteFeatures = {
|
|
|
22
22
|
showFeedbackWidget?: boolean
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export type ApiSpecEntry = {
|
|
26
|
+
label: string
|
|
27
|
+
file: string
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export type ApiReferenceConfig = {
|
|
26
31
|
enabled: boolean
|
|
27
|
-
|
|
32
|
+
specs: Record<string, ApiSpecEntry>
|
|
28
33
|
navLabel?: string
|
|
29
34
|
languages?: string[]
|
|
30
35
|
roles?: string[]
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
export type LlmsConfig = {
|
|
39
|
+
enabled?: boolean
|
|
40
|
+
directive?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export type CamelMindConfig = {
|
|
34
44
|
title: string
|
|
35
45
|
tagline: string
|
|
@@ -43,4 +53,5 @@ export type CamelMindConfig = {
|
|
|
43
53
|
}
|
|
44
54
|
site?: SiteFeatures
|
|
45
55
|
apiReference?: ApiReferenceConfig
|
|
56
|
+
llms?: LlmsConfig
|
|
46
57
|
}
|
package/template/lib/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import siteConfig from "@/camelmind.config"
|
|
2
2
|
import type { AuthConfig, CamelMindConfig } from "./config-types"
|
|
3
3
|
|
|
4
|
-
export type { AuthConfig, AuthProvider, CamelMindConfig, OidcConfig } from "./config-types"
|
|
4
|
+
export type { AuthConfig, AuthProvider, CamelMindConfig, LlmsConfig, OidcConfig } from "./config-types"
|
|
5
5
|
|
|
6
6
|
let _config: CamelMindConfig | null = null
|
|
7
7
|
|
package/template/lib/mdx.ts
CHANGED
|
@@ -44,6 +44,19 @@ function extractToc(source: string): TocEntry[] {
|
|
|
44
44
|
return entries
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function processForLLM(source: string): string {
|
|
48
|
+
let result = source.replace(/<LLMIgnore>[\s\S]*?<\/LLMIgnore>/g, "")
|
|
49
|
+
result = result.replace(/<LLMOnly>([\s\S]*?)<\/LLMOnly>/g, "$1")
|
|
50
|
+
return result.replace(/\n{3,}/g, "\n\n").trim()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadFrontmatterOnly(filePath: string): FrontMatter {
|
|
54
|
+
const fullPath = path.join(process.cwd(), filePath)
|
|
55
|
+
const raw = fs.readFileSync(fullPath, "utf-8")
|
|
56
|
+
const { data } = matter(raw)
|
|
57
|
+
return data as FrontMatter
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
export function loadMdxFile(filePath: string): DocContent {
|
|
48
61
|
const fullPath = path.join(process.cwd(), filePath)
|
|
49
62
|
const raw = fs.readFileSync(fullPath, "utf-8")
|
package/template/lib/nav.ts
CHANGED
|
@@ -8,11 +8,59 @@ export { isNavGroup } from "./nav-types"
|
|
|
8
8
|
|
|
9
9
|
let _navCache: NavConfig | null = null
|
|
10
10
|
|
|
11
|
+
function stripPrefix(slug: string, prefix: string): string {
|
|
12
|
+
if (slug.startsWith(prefix + "/")) return slug.slice(prefix.length)
|
|
13
|
+
if (slug === prefix) return "/"
|
|
14
|
+
return slug
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stripNavPrefix(nav: NavConfig, prefix: string): NavConfig {
|
|
18
|
+
const s = (slug: string) => stripPrefix(slug, prefix)
|
|
19
|
+
|
|
20
|
+
const walkChildren = (children: NavChild[]): NavChild[] =>
|
|
21
|
+
children.map((c) => ({
|
|
22
|
+
...c,
|
|
23
|
+
slug: s(c.slug),
|
|
24
|
+
children: c.children ? walkChildren(c.children) : undefined,
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const walkEntries = (entries: NavEntry[]): NavEntry[] =>
|
|
28
|
+
entries.map((e) => ({
|
|
29
|
+
...e,
|
|
30
|
+
slug: s(e.slug),
|
|
31
|
+
section: e.section ? walkChildren(e.section) : undefined,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
nav: nav.nav.map((item) => {
|
|
36
|
+
if ("dropdown" in item) {
|
|
37
|
+
const group = item as NavGroup
|
|
38
|
+
return {
|
|
39
|
+
...group,
|
|
40
|
+
...(group.slug ? { slug: s(group.slug) } : {}),
|
|
41
|
+
items: group.items ? walkEntries(group.items) : undefined,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const entry = item as NavEntry
|
|
45
|
+
return { ...entry, slug: s(entry.slug) }
|
|
46
|
+
}),
|
|
47
|
+
} as NavConfig
|
|
48
|
+
}
|
|
49
|
+
|
|
11
50
|
export function loadNav(): NavConfig {
|
|
12
51
|
if (process.env.NODE_ENV !== "development" && _navCache) return _navCache
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
52
|
+
|
|
53
|
+
// Derive the unversioned nav from the first stable version in versions.yml,
|
|
54
|
+
// stripping the version prefix from all slugs so /v2/foo becomes /foo.
|
|
55
|
+
const versionsPath = path.join(process.cwd(), "versions.yml")
|
|
56
|
+
const { versions } = yaml.load(fs.readFileSync(versionsPath, "utf-8")) as {
|
|
57
|
+
versions: { id: string; stable: boolean; nav: string }[]
|
|
58
|
+
}
|
|
59
|
+
const latest = versions.find((v) => v.stable) ?? versions[0]
|
|
60
|
+
const navPath = path.join(process.cwd(), latest.nav)
|
|
61
|
+
const nav = yaml.load(fs.readFileSync(navPath, "utf-8")) as NavConfig
|
|
62
|
+
|
|
63
|
+
_navCache = stripNavPrefix(nav, `/${latest.id}`)
|
|
16
64
|
return _navCache
|
|
17
65
|
}
|
|
18
66
|
|
|
@@ -123,3 +171,24 @@ export function getSectionForSlugFromConfig(nav: NavConfig, slug: string): NavEn
|
|
|
123
171
|
export function getSectionForSlug(slug: string): NavEntry | null {
|
|
124
172
|
return getSectionForSlugFromConfig(loadNav(), slug)
|
|
125
173
|
}
|
|
174
|
+
|
|
175
|
+
export function getAllPublicEntries(nav: NavConfig): (NavEntry | NavChild)[] {
|
|
176
|
+
const entries: (NavEntry | NavChild)[] = []
|
|
177
|
+
for (const item of nav.nav) {
|
|
178
|
+
if ("dropdown" in item) {
|
|
179
|
+
const group = item as NavGroup
|
|
180
|
+
for (const entry of (group.items ?? [])) {
|
|
181
|
+
if (entry.roles.length === 0) entries.push(entry)
|
|
182
|
+
if (entry.section) {
|
|
183
|
+
for (const child of flattenChildren(entry.section)) {
|
|
184
|
+
if (child.roles.length === 0) entries.push(child)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else if ("slug" in item) {
|
|
189
|
+
const entry = item as NavEntry
|
|
190
|
+
if (entry.roles.length === 0) entries.push(entry)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return entries
|
|
194
|
+
}
|
package/template/lib/versions.ts
CHANGED
|
@@ -3,20 +3,24 @@ import path from "path"
|
|
|
3
3
|
import yaml from "js-yaml"
|
|
4
4
|
import { loadNav } from "./nav"
|
|
5
5
|
import type { NavConfig } from "./nav-types"
|
|
6
|
+
import type { ApiReferenceConfig } from "./config-types"
|
|
6
7
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
languages?: string[]
|
|
8
|
+
export type VersionApiReferenceTab = {
|
|
9
|
+
id: string
|
|
10
|
+
spec: string // key into camelmind.config.ts apiReference.specs
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type VersionApiReference =
|
|
14
|
+
| { spec: string } // single named spec
|
|
15
|
+
| { tabs: VersionApiReferenceTab[] } // multi-spec tabs
|
|
16
|
+
|
|
13
17
|
export type Version = {
|
|
14
18
|
id: string
|
|
15
19
|
label: string
|
|
16
20
|
stable: boolean
|
|
17
21
|
badge?: string
|
|
18
22
|
nav: string
|
|
19
|
-
// true
|
|
23
|
+
// false = disabled, omitted/true = use first spec in registry, object = explicit config
|
|
20
24
|
api_reference?: boolean | VersionApiReference
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -24,6 +28,18 @@ export type VersionsConfig = {
|
|
|
24
28
|
versions: Version[]
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
// Resolved output types
|
|
32
|
+
export type ResolvedTab = {
|
|
33
|
+
id: string
|
|
34
|
+
label: string
|
|
35
|
+
file: string
|
|
36
|
+
languages?: string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ResolvedApiReference =
|
|
40
|
+
| { mode: "single"; label: string; file: string; languages?: string[] }
|
|
41
|
+
| { mode: "tabs"; tabs: ResolvedTab[] }
|
|
42
|
+
|
|
27
43
|
let _versionsCache: VersionsConfig | null = null
|
|
28
44
|
|
|
29
45
|
export function loadVersions(): VersionsConfig {
|
|
@@ -33,12 +49,52 @@ export function loadVersions(): VersionsConfig {
|
|
|
33
49
|
return _versionsCache
|
|
34
50
|
}
|
|
35
51
|
|
|
52
|
+
// Resolve which spec(s) to show for a given version.
|
|
53
|
+
// Resolution: versions.yml api_reference → apiReference.specs registry → first spec in registry
|
|
54
|
+
// Returns null if api_reference is explicitly disabled for this version.
|
|
55
|
+
export function getApiReferenceForVersion(
|
|
56
|
+
versionId: string | null,
|
|
57
|
+
config: ApiReferenceConfig
|
|
58
|
+
): ResolvedApiReference | null {
|
|
59
|
+
const { versions } = loadVersions()
|
|
60
|
+
|
|
61
|
+
let versionAr: boolean | VersionApiReference | undefined
|
|
62
|
+
if (versionId) {
|
|
63
|
+
const v = versions.find((v) => v.id === versionId)
|
|
64
|
+
versionAr = v?.api_reference
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (versionAr === false) return null
|
|
68
|
+
|
|
69
|
+
// Default: use first spec in registry
|
|
70
|
+
if (!versionAr || versionAr === true) {
|
|
71
|
+
const firstEntry = Object.entries(config.specs)[0]
|
|
72
|
+
if (!firstEntry) return null
|
|
73
|
+
const [, entry] = firstEntry
|
|
74
|
+
return { mode: "single", label: entry.label, file: entry.file, languages: config.languages }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Tabs mode
|
|
78
|
+
if ("tabs" in versionAr) {
|
|
79
|
+
const tabs: ResolvedTab[] = versionAr.tabs.flatMap((tab) => {
|
|
80
|
+
const entry = config.specs[tab.spec]
|
|
81
|
+
if (!entry) return []
|
|
82
|
+
return [{ id: tab.id, label: entry.label, file: entry.file, languages: config.languages }]
|
|
83
|
+
})
|
|
84
|
+
return tabs.length ? { mode: "tabs", tabs } : null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Single named spec
|
|
88
|
+
const entry = config.specs[versionAr.spec]
|
|
89
|
+
if (!entry) return null
|
|
90
|
+
return { mode: "single", label: entry.label, file: entry.file, languages: config.languages }
|
|
91
|
+
}
|
|
92
|
+
|
|
36
93
|
export function getVersionFromSlug(slug: string): string | null {
|
|
37
94
|
const { versions } = loadVersions()
|
|
38
95
|
for (const v of versions) {
|
|
39
96
|
if (slug.startsWith(`/${v.id}/`) || slug === `/${v.id}`) return v.id
|
|
40
97
|
}
|
|
41
|
-
// no version prefix = default (latest)
|
|
42
98
|
return null
|
|
43
99
|
}
|
|
44
100
|
|
|
@@ -57,30 +113,10 @@ export function getLatestVersion(): Version {
|
|
|
57
113
|
return versions[0]
|
|
58
114
|
}
|
|
59
115
|
|
|
60
|
-
// Resolve which OpenAPI spec to use for a given version, with site-level fallback.
|
|
61
|
-
// Returns null if api_reference is explicitly disabled for this version.
|
|
62
|
-
export function getApiSpecForVersion(
|
|
63
|
-
versionId: string | null,
|
|
64
|
-
siteSpec: string,
|
|
65
|
-
siteLanguages?: string[]
|
|
66
|
-
): { spec: string; languages?: string[] } | null {
|
|
67
|
-
if (!versionId) return { spec: siteSpec, languages: siteLanguages }
|
|
68
|
-
const { versions } = loadVersions()
|
|
69
|
-
const v = versions.find((v) => v.id === versionId)
|
|
70
|
-
if (!v) return { spec: siteSpec, languages: siteLanguages }
|
|
71
|
-
|
|
72
|
-
const ar = v.api_reference
|
|
73
|
-
if (ar === false) return null
|
|
74
|
-
if (!ar || ar === true) return { spec: siteSpec, languages: siteLanguages }
|
|
75
|
-
return { spec: ar.spec, languages: ar.languages ?? siteLanguages }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Given a slug like /v1.9/getting-started/overview, return /getting-started/overview
|
|
79
116
|
export function stripVersionPrefix(slug: string, versionId: string): string {
|
|
80
117
|
return slug.replace(new RegExp(`^/${versionId}`), "") || "/"
|
|
81
118
|
}
|
|
82
119
|
|
|
83
|
-
// Given /getting-started/overview and a target version, return /v1.9/getting-started/overview
|
|
84
120
|
export function swapVersion(slug: string, currentVersionId: string | null, targetVersionId: string): string {
|
|
85
121
|
const bare = currentVersionId ? stripVersionPrefix(slug, currentVersionId) : slug
|
|
86
122
|
return `/${targetVersionId}${bare}`
|
package/template/proxy.ts
CHANGED
|
@@ -6,6 +6,14 @@ import { isAuthEnabled, isPrivateSite, isPublicPath } from "@/lib/config"
|
|
|
6
6
|
export async function proxy(req: NextRequest) {
|
|
7
7
|
const { pathname } = req.nextUrl
|
|
8
8
|
|
|
9
|
+
// Rewrite /<slug>.md to /api/llms/<slug> before auth runs.
|
|
10
|
+
// /api/llms is in publicPaths so the handler is always reachable.
|
|
11
|
+
if (pathname.endsWith(".md") && !pathname.startsWith("/api/")) {
|
|
12
|
+
const url = req.nextUrl.clone()
|
|
13
|
+
url.pathname = `/api/llms${pathname.slice(0, -3)}`
|
|
14
|
+
return NextResponse.rewrite(url)
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
// Auth disabled — all traffic passes through
|
|
10
18
|
if (!isAuthEnabled()) {
|
|
11
19
|
return NextResponse.next()
|
|
@@ -46,5 +54,6 @@ export async function proxy(req: NextRequest) {
|
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export const config = {
|
|
49
|
-
|
|
57
|
+
// Exclude Next.js internals and common static asset extensions served from public/
|
|
58
|
+
matcher: ["/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|eot|otf)).*)"],
|
|
50
59
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
VERSION=${1:-
|
|
13
|
+
VERSION=${1:-latest}
|
|
14
14
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
15
15
|
OUT_DIR="$ROOT/out"
|
|
16
16
|
BUILDS_DIR="$ROOT/offline-builds"
|
|
@@ -21,6 +21,8 @@ SERVER_ROUTES=(
|
|
|
21
21
|
"app/api/raw/route.ts"
|
|
22
22
|
"app/api/download/route.ts"
|
|
23
23
|
"app/api/search/route.ts"
|
|
24
|
+
"app/api/feedback/route.ts"
|
|
25
|
+
"app/api/llms"
|
|
24
26
|
"app/api/auth"
|
|
25
27
|
)
|
|
26
28
|
|
|
@@ -48,7 +50,17 @@ restore_routes() {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
# Always restore on exit (even on error)
|
|
51
|
-
|
|
53
|
+
PAGES_DIR="$ROOT/.pdf-pages"
|
|
54
|
+
|
|
55
|
+
cleanup() {
|
|
56
|
+
restore_routes
|
|
57
|
+
rm -rf "$PAGES_DIR"
|
|
58
|
+
# Kill the static server if still running
|
|
59
|
+
if [[ -n "${STATIC_PID:-}" ]]; then
|
|
60
|
+
kill "$STATIC_PID" 2>/dev/null || true
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
trap cleanup EXIT
|
|
52
64
|
|
|
53
65
|
echo "▶ Building offline package for version: $VERSION"
|
|
54
66
|
|
|
@@ -69,10 +81,14 @@ OFFLINE_MODE=true TARGET_VERSION="$VERSION" npx next build
|
|
|
69
81
|
|
|
70
82
|
cat > "$OUT_DIR/launch.sh" <<'LAUNCHER'
|
|
71
83
|
#!/usr/bin/env bash
|
|
72
|
-
#
|
|
84
|
+
# CamelMind offline launcher (Mac / Linux)
|
|
85
|
+
cd "$(dirname "$0")"
|
|
73
86
|
PORT=8765
|
|
74
87
|
URL="http://localhost:$PORT/home/"
|
|
75
88
|
|
|
89
|
+
# Free the port if a previous server is still running
|
|
90
|
+
lsof -ti:$PORT 2>/dev/null | xargs kill 2>/dev/null || true
|
|
91
|
+
|
|
76
92
|
# Try Python 3 first (available on most systems without internet)
|
|
77
93
|
if command -v python3 &>/dev/null; then
|
|
78
94
|
echo "Starting local server on $URL"
|
|
@@ -113,7 +129,8 @@ chmod +x "$OUT_DIR/launch.sh"
|
|
|
113
129
|
|
|
114
130
|
cat > "$OUT_DIR/launch.bat" <<'LAUNCHER'
|
|
115
131
|
@echo off
|
|
116
|
-
REM
|
|
132
|
+
REM CamelMind offline launcher (Windows)
|
|
133
|
+
cd /D "%~dp0"
|
|
117
134
|
set PORT=8765
|
|
118
135
|
set URL=http://localhost:%PORT%/home/
|
|
119
136
|
|
|
@@ -180,7 +197,28 @@ NOTES
|
|
|
180
197
|
Do not redistribute without authorization.
|
|
181
198
|
EOF
|
|
182
199
|
|
|
183
|
-
# 4.
|
|
200
|
+
# 4. Generate PDF from the static export
|
|
201
|
+
echo " → Generating PDF..."
|
|
202
|
+
STATIC_PORT=8766
|
|
203
|
+
|
|
204
|
+
# Serve the static output on a temporary port
|
|
205
|
+
python3 -m http.server "$STATIC_PORT" --directory "$OUT_DIR" >/dev/null 2>&1 &
|
|
206
|
+
STATIC_PID=$!
|
|
207
|
+
|
|
208
|
+
# Wait up to 10s for the server to be ready
|
|
209
|
+
for i in $(seq 1 20); do
|
|
210
|
+
if curl -s -o /dev/null "http://localhost:$STATIC_PORT/home/"; then break; fi
|
|
211
|
+
sleep 0.5
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
npx tsx scripts/generate-pdfs.ts --port="$STATIC_PORT" --out="$PAGES_DIR" --no-auth
|
|
215
|
+
npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$BUILDS_DIR"
|
|
216
|
+
|
|
217
|
+
kill "$STATIC_PID" 2>/dev/null || true
|
|
218
|
+
unset STATIC_PID
|
|
219
|
+
rm -rf "$PAGES_DIR"
|
|
220
|
+
|
|
221
|
+
# 5. Zip the output
|
|
184
222
|
echo " → Packaging..."
|
|
185
223
|
mkdir -p "$BUILDS_DIR"
|
|
186
224
|
cd "$OUT_DIR"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
|
-
VERSION=${1:-
|
|
12
|
+
VERSION=${1:-latest}
|
|
13
13
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
14
|
PAGES_DIR="$ROOT/.pdf-pages"
|
|
15
15
|
PORT=3000
|
|
@@ -37,5 +37,5 @@ npx tsx scripts/generate-pdfs.ts --port="$PORT" --out="$PAGES_DIR"
|
|
|
37
37
|
echo " → Assembling master PDF..."
|
|
38
38
|
npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$ROOT/offline-builds"
|
|
39
39
|
|
|
40
|
-
SIZE=$(du -sh "$ROOT/offline-builds/
|
|
41
|
-
echo "✓ Done: offline-builds/
|
|
40
|
+
SIZE=$(du -sh "$ROOT/offline-builds/camelmind-${VERSION}.pdf" | cut -f1)
|
|
41
|
+
echo "✓ Done: offline-builds/camelmind-${VERSION}.pdf ($SIZE)"
|
|
@@ -10,9 +10,15 @@ import path from "path"
|
|
|
10
10
|
import matter from "gray-matter"
|
|
11
11
|
|
|
12
12
|
const ROOT = path.resolve(process.cwd())
|
|
13
|
-
const NAV_FILE = path.join(ROOT, "nav", "nav.yml")
|
|
14
13
|
const OUT_FILE = path.join(ROOT, "public", "search-index.json")
|
|
15
14
|
|
|
15
|
+
// Resolve the latest stable version's nav file from versions.yml
|
|
16
|
+
const { versions } = yaml.load(fs.readFileSync(path.join(ROOT, "versions.yml"), "utf-8")) as {
|
|
17
|
+
versions: { id: string; stable: boolean; nav: string }[]
|
|
18
|
+
}
|
|
19
|
+
const latestVersion = versions.find((v) => v.stable) ?? versions[0]
|
|
20
|
+
const NAV_FILE = path.join(ROOT, latestVersion.nav)
|
|
21
|
+
|
|
16
22
|
// Minimal YAML parser for our nav structure (avoids pulling in the full Next.js lib graph)
|
|
17
23
|
import yaml from "js-yaml"
|
|
18
24
|
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
import { PDFDocument, StandardFonts, rgb, PDFPage } from "pdf-lib"
|
|
13
13
|
import fs from "fs"
|
|
14
14
|
import path from "path"
|
|
15
|
+
import config from "../camelmind.config"
|
|
16
|
+
|
|
17
|
+
const SITE_TITLE = config.title
|
|
15
18
|
|
|
16
19
|
const ROOT = path.resolve(process.cwd())
|
|
17
20
|
|
|
@@ -49,25 +52,15 @@ async function buildCoverPage(doc: PDFDocument): Promise<PDFPage> {
|
|
|
49
52
|
// Thin accent line
|
|
50
53
|
page.drawRectangle({ x: MARGIN, y: PH * 0.45 - 2, width: PW - MARGIN * 2, height: 2, color: rgb(0.3, 0.35, 0.45) })
|
|
51
54
|
|
|
52
|
-
// "SECOND FRONT SYSTEMS" label
|
|
53
|
-
page.drawText("SECOND FRONT SYSTEMS", {
|
|
54
|
-
x: MARGIN,
|
|
55
|
-
y: PH * 0.45 + 180,
|
|
56
|
-
size: 9,
|
|
57
|
-
font: regularFont,
|
|
58
|
-
color: rgb(0.6, 0.65, 0.75),
|
|
59
|
-
characterSpacing: 2,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
55
|
// Main title
|
|
63
|
-
page.drawText(
|
|
56
|
+
page.drawText(SITE_TITLE, {
|
|
64
57
|
x: MARGIN,
|
|
65
58
|
y: PH * 0.45 + 130,
|
|
66
59
|
size: 42,
|
|
67
60
|
font: boldFont,
|
|
68
61
|
color: WHITE,
|
|
69
62
|
})
|
|
70
|
-
page.drawText("
|
|
63
|
+
page.drawText("Documentation", {
|
|
71
64
|
x: MARGIN,
|
|
72
65
|
y: PH * 0.45 + 78,
|
|
73
66
|
size: 42,
|
|
@@ -94,13 +87,12 @@ async function buildCoverPage(doc: PDFDocument): Promise<PDFPage> {
|
|
|
94
87
|
|
|
95
88
|
// Footer bar
|
|
96
89
|
page.drawRectangle({ x: 0, y: 0, width: PW, height: 36, color: LIGHT })
|
|
97
|
-
page.drawText(
|
|
90
|
+
page.drawText(`${SITE_TITLE.toLowerCase()} documentation`, {
|
|
98
91
|
x: MARGIN,
|
|
99
92
|
y: 12,
|
|
100
93
|
size: 8,
|
|
101
94
|
font: regularFont,
|
|
102
95
|
color: MID,
|
|
103
|
-
characterSpacing: 0.5,
|
|
104
96
|
})
|
|
105
97
|
page.drawText("CONFIDENTIAL — FOR AUTHORIZED USERS ONLY", {
|
|
106
98
|
x: PW - MARGIN - 220,
|
|
@@ -126,7 +118,7 @@ async function buildTocPage(
|
|
|
126
118
|
|
|
127
119
|
const drawPageHeader = (p: PDFPage) => {
|
|
128
120
|
p.drawText("TABLE OF CONTENTS", {
|
|
129
|
-
x: MARGIN, y: PH - MARGIN + 10, size: 8, font: regularFont, color: MID,
|
|
121
|
+
x: MARGIN, y: PH - MARGIN + 10, size: 8, font: regularFont, color: MID,
|
|
130
122
|
})
|
|
131
123
|
p.drawLine({ start: { x: MARGIN, y: PH - MARGIN - 4 }, end: { x: PW - MARGIN, y: PH - MARGIN - 4 }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) })
|
|
132
124
|
}
|
|
@@ -152,7 +144,7 @@ async function buildTocPage(
|
|
|
152
144
|
if (entry.group !== lastGroup) {
|
|
153
145
|
if (lastGroup !== "") y -= 6
|
|
154
146
|
page.drawText(entry.group.toUpperCase(), {
|
|
155
|
-
x: MARGIN, y, size: 7.5, font: boldFont, color: MID,
|
|
147
|
+
x: MARGIN, y, size: 7.5, font: boldFont, color: MID,
|
|
156
148
|
})
|
|
157
149
|
y -= 18
|
|
158
150
|
lastGroup = entry.group
|
|
@@ -249,7 +241,7 @@ async function main() {
|
|
|
249
241
|
|
|
250
242
|
// ── Write output ───────────────────────────────────────────────────────────
|
|
251
243
|
fs.mkdirSync(OUT_DIR, { recursive: true })
|
|
252
|
-
const outFile = path.join(OUT_DIR, `
|
|
244
|
+
const outFile = path.join(OUT_DIR, `camelmind-${VERSION}.pdf`)
|
|
253
245
|
const pdfBytes = await masterDoc.save()
|
|
254
246
|
fs.writeFileSync(outFile, pdfBytes)
|
|
255
247
|
|