@raystack/chronicle 0.1.2

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 (101) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +9763 -0
  3. package/next.config.mjs +10 -0
  4. package/package.json +60 -0
  5. package/source.config.ts +43 -0
  6. package/src/app/[[...slug]]/layout.tsx +15 -0
  7. package/src/app/[[...slug]]/page.tsx +57 -0
  8. package/src/app/api/apis-proxy/route.ts +59 -0
  9. package/src/app/api/health/route.ts +3 -0
  10. package/src/app/api/search/route.ts +90 -0
  11. package/src/app/apis/[[...slug]]/layout.module.css +22 -0
  12. package/src/app/apis/[[...slug]]/layout.tsx +26 -0
  13. package/src/app/apis/[[...slug]]/page.tsx +57 -0
  14. package/src/app/layout.tsx +26 -0
  15. package/src/app/providers.tsx +8 -0
  16. package/src/cli/commands/build.ts +31 -0
  17. package/src/cli/commands/dev.ts +32 -0
  18. package/src/cli/commands/init.ts +58 -0
  19. package/src/cli/commands/serve.ts +52 -0
  20. package/src/cli/commands/start.ts +32 -0
  21. package/src/cli/index.ts +21 -0
  22. package/src/cli/utils/config.ts +35 -0
  23. package/src/cli/utils/index.ts +2 -0
  24. package/src/cli/utils/process.ts +7 -0
  25. package/src/components/api/code-snippets.module.css +7 -0
  26. package/src/components/api/code-snippets.tsx +76 -0
  27. package/src/components/api/endpoint-page.module.css +58 -0
  28. package/src/components/api/endpoint-page.tsx +283 -0
  29. package/src/components/api/field-row.module.css +126 -0
  30. package/src/components/api/field-row.tsx +204 -0
  31. package/src/components/api/field-section.module.css +24 -0
  32. package/src/components/api/field-section.tsx +100 -0
  33. package/src/components/api/index.ts +8 -0
  34. package/src/components/api/json-editor.module.css +9 -0
  35. package/src/components/api/json-editor.tsx +61 -0
  36. package/src/components/api/key-value-editor.module.css +13 -0
  37. package/src/components/api/key-value-editor.tsx +62 -0
  38. package/src/components/api/method-badge.module.css +4 -0
  39. package/src/components/api/method-badge.tsx +29 -0
  40. package/src/components/api/response-panel.module.css +8 -0
  41. package/src/components/api/response-panel.tsx +44 -0
  42. package/src/components/common/breadcrumb.tsx +3 -0
  43. package/src/components/common/button.tsx +3 -0
  44. package/src/components/common/callout.module.css +7 -0
  45. package/src/components/common/callout.tsx +27 -0
  46. package/src/components/common/code-block.tsx +3 -0
  47. package/src/components/common/dialog.tsx +3 -0
  48. package/src/components/common/index.ts +10 -0
  49. package/src/components/common/input-field.tsx +3 -0
  50. package/src/components/common/sidebar.tsx +3 -0
  51. package/src/components/common/switch.tsx +3 -0
  52. package/src/components/common/table.tsx +3 -0
  53. package/src/components/common/tabs.tsx +3 -0
  54. package/src/components/mdx/code.module.css +42 -0
  55. package/src/components/mdx/code.tsx +27 -0
  56. package/src/components/mdx/details.module.css +37 -0
  57. package/src/components/mdx/details.tsx +18 -0
  58. package/src/components/mdx/image.tsx +38 -0
  59. package/src/components/mdx/index.tsx +35 -0
  60. package/src/components/mdx/link.tsx +38 -0
  61. package/src/components/mdx/mermaid.module.css +9 -0
  62. package/src/components/mdx/mermaid.tsx +37 -0
  63. package/src/components/mdx/paragraph.module.css +8 -0
  64. package/src/components/mdx/paragraph.tsx +19 -0
  65. package/src/components/mdx/table.tsx +40 -0
  66. package/src/components/ui/breadcrumbs.tsx +72 -0
  67. package/src/components/ui/client-theme-switcher.tsx +18 -0
  68. package/src/components/ui/footer.module.css +27 -0
  69. package/src/components/ui/footer.tsx +30 -0
  70. package/src/components/ui/search.module.css +104 -0
  71. package/src/components/ui/search.tsx +202 -0
  72. package/src/lib/api-routes.ts +120 -0
  73. package/src/lib/config.ts +36 -0
  74. package/src/lib/index.ts +2 -0
  75. package/src/lib/openapi.ts +188 -0
  76. package/src/lib/remark-unused-directives.ts +31 -0
  77. package/src/lib/schema.ts +99 -0
  78. package/src/lib/snippet-generators.ts +87 -0
  79. package/src/lib/source.ts +66 -0
  80. package/src/themes/default/Layout.module.css +81 -0
  81. package/src/themes/default/Layout.tsx +133 -0
  82. package/src/themes/default/Page.module.css +46 -0
  83. package/src/themes/default/Page.tsx +21 -0
  84. package/src/themes/default/Toc.module.css +48 -0
  85. package/src/themes/default/Toc.tsx +66 -0
  86. package/src/themes/default/font.ts +6 -0
  87. package/src/themes/default/index.ts +13 -0
  88. package/src/themes/paper/ChapterNav.module.css +71 -0
  89. package/src/themes/paper/ChapterNav.tsx +96 -0
  90. package/src/themes/paper/Layout.module.css +33 -0
  91. package/src/themes/paper/Layout.tsx +25 -0
  92. package/src/themes/paper/Page.module.css +174 -0
  93. package/src/themes/paper/Page.tsx +107 -0
  94. package/src/themes/paper/ReadingProgress.module.css +132 -0
  95. package/src/themes/paper/ReadingProgress.tsx +294 -0
  96. package/src/themes/paper/index.ts +8 -0
  97. package/src/themes/registry.ts +14 -0
  98. package/src/types/config.ts +64 -0
  99. package/src/types/content.ts +35 -0
  100. package/src/types/index.ts +3 -0
  101. package/src/types/theme.ts +22 -0
@@ -0,0 +1,10 @@
1
+ import { createMDX } from 'fumadocs-mdx/next'
2
+
3
+ const withMDX = createMDX()
4
+
5
+ /** @type {import('next').NextConfig} */
6
+ const nextConfig = {
7
+ reactStrictMode: true,
8
+ }
9
+
10
+ export default withMDX(nextConfig)
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@raystack/chronicle",
3
+ "version": "0.1.2",
4
+ "description": "Config-driven documentation framework",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "files": [
8
+ "bin",
9
+ "dist",
10
+ "src",
11
+ "templates",
12
+ "next.config.mjs",
13
+ "source.config.ts"
14
+ ],
15
+ "bin": {
16
+ "chronicle": "./bin/chronicle.js"
17
+ },
18
+ "scripts": {
19
+ "build:cli": "bun build-cli.ts"
20
+ },
21
+ "devDependencies": {
22
+ "@biomejs/biome": "^2.3.13",
23
+ "@raystack/tools-config": "0.56.0",
24
+ "@types/lodash": "^4.17.23",
25
+ "@types/mdx": "^2.0.13",
26
+ "@types/node": "^25.1.0",
27
+ "@types/react": "^19.2.10",
28
+ "@types/react-dom": "^19.2.3",
29
+ "@types/semver": "^7.7.1",
30
+ "openapi-types": "^12.1.3",
31
+ "semver": "^7.7.4",
32
+ "typescript": "5.9.3"
33
+ },
34
+ "dependencies": {
35
+ "@codemirror/lang-json": "^6.0.2",
36
+ "@codemirror/state": "^6.5.4",
37
+ "@codemirror/theme-one-dark": "^6.1.3",
38
+ "@codemirror/view": "^6.39.14",
39
+ "@heroicons/react": "^2.2.0",
40
+ "@raystack/apsara": "^0.56.0",
41
+ "chalk": "^5.6.2",
42
+ "class-variance-authority": "^0.7.1",
43
+ "codemirror": "^6.0.2",
44
+ "commander": "^14.0.2",
45
+ "fumadocs-core": "^16.4.9",
46
+ "fumadocs-mdx": "^14.2.6",
47
+ "lodash": "^4.17.23",
48
+ "mermaid": "^11.13.0",
49
+ "next": "16.1.6",
50
+ "react": "^19.0.0",
51
+ "react-device-detect": "^2.2.3",
52
+ "react-dom": "^19.0.0",
53
+ "remark-attr": "^0.11.1",
54
+ "remark-directive": "^4.0.0",
55
+ "slugify": "^1.6.6",
56
+ "unist-util-visit": "^5.1.0",
57
+ "yaml": "^2.8.2",
58
+ "zod": "^4.3.6"
59
+ }
60
+ }
@@ -0,0 +1,43 @@
1
+ import { defineDocs, defineConfig } from 'fumadocs-mdx/config'
2
+ import remarkDirective from 'remark-directive'
3
+ import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins'
4
+ import remarkUnusedDirectives from './src/lib/remark-unused-directives'
5
+
6
+ const contentDir = process.env.CHRONICLE_CONTENT_DIR || './content'
7
+
8
+ export const docs = defineDocs({
9
+ dir: contentDir,
10
+ docs: {
11
+ files: ['**/*.mdx', '**/*.md', '!**/node_modules/**'],
12
+ },
13
+ })
14
+
15
+ export default defineConfig({
16
+ mdxOptions: {
17
+ remarkPlugins: [
18
+ remarkDirective,
19
+ [
20
+ remarkDirectiveAdmonition,
21
+ {
22
+ tags: {
23
+ CalloutContainer: 'Callout',
24
+ CalloutTitle: 'CalloutTitle',
25
+ CalloutDescription: 'CalloutDescription',
26
+ },
27
+ types: {
28
+ note: 'accent',
29
+ tip: 'accent',
30
+ info: 'accent',
31
+ warn: 'attention',
32
+ warning: 'attention',
33
+ danger: 'alert',
34
+ caution: 'alert',
35
+ success: 'success',
36
+ },
37
+ },
38
+ ],
39
+ remarkUnusedDirectives,
40
+ remarkMdxMermaid,
41
+ ],
42
+ },
43
+ })
@@ -0,0 +1,15 @@
1
+ import { loadConfig } from '@/lib/config'
2
+ import { buildPageTree } from '@/lib/source'
3
+ import { getTheme } from '@/themes/registry'
4
+
5
+ export default function DocsLayout({ children }: { children: React.ReactNode }) {
6
+ const config = loadConfig()
7
+ const tree = buildPageTree()
8
+ const { Layout, className } = getTheme(config.theme?.name)
9
+
10
+ return (
11
+ <Layout config={config} tree={tree} classNames={{ layout: className }}>
12
+ {children}
13
+ </Layout>
14
+ )
15
+ }
@@ -0,0 +1,57 @@
1
+ import { notFound } from 'next/navigation'
2
+ import type { MDXContent } from 'mdx/types'
3
+ import { loadConfig } from '@/lib/config'
4
+ import { source, buildPageTree } from '@/lib/source'
5
+ import { getTheme } from '@/themes/registry'
6
+ import { mdxComponents } from '@/components/mdx'
7
+
8
+ interface PageProps {
9
+ params: Promise<{ slug?: string[] }>
10
+ }
11
+
12
+ interface PageData {
13
+ title: string
14
+ description?: string
15
+ body: MDXContent
16
+ toc: { title: string; url: string; depth: number }[]
17
+ }
18
+
19
+ export default async function DocsPage({ params }: PageProps) {
20
+ const { slug } = await params
21
+ const config = loadConfig()
22
+
23
+ const page = source.getPage(slug)
24
+
25
+ if (!page) {
26
+ notFound()
27
+ }
28
+
29
+ const { Page } = getTheme(config.theme?.name)
30
+
31
+ const data = page.data as PageData
32
+ const MDXBody = data.body
33
+
34
+ const tree = buildPageTree()
35
+
36
+ return (
37
+ <Page
38
+ page={{
39
+ slug: slug ?? [],
40
+ frontmatter: {
41
+ title: data.title,
42
+ description: data.description,
43
+ },
44
+ content: <MDXBody components={mdxComponents} />,
45
+ toc: data.toc ?? [],
46
+ }}
47
+ config={config}
48
+ tree={tree}
49
+ />
50
+ )
51
+ }
52
+
53
+ export function generateStaticParams() {
54
+ return source.getPages().map((page) => ({
55
+ slug: page.slugs,
56
+ }))
57
+ }
@@ -0,0 +1,59 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { loadConfig } from "@/lib/config";
3
+ import { loadApiSpecs } from "@/lib/openapi";
4
+
5
+ export async function POST(request: NextRequest) {
6
+ const { specName, method, path, headers, body } = await request.json();
7
+
8
+ if (!specName || !method || !path) {
9
+ return NextResponse.json(
10
+ { error: "Missing specName, method, or path" },
11
+ { status: 400 },
12
+ );
13
+ }
14
+
15
+ const config = loadConfig();
16
+ const specs = loadApiSpecs(config.api ?? []);
17
+ const spec = specs.find((s) => s.name === specName);
18
+
19
+ if (!spec) {
20
+ return NextResponse.json(
21
+ { error: `Unknown spec: ${specName}` },
22
+ { status: 404 },
23
+ );
24
+ }
25
+
26
+ const url = spec.server.url + path;
27
+
28
+ try {
29
+ const response = await fetch(url, {
30
+ method,
31
+ headers,
32
+ body: body ? JSON.stringify(body) : undefined,
33
+ });
34
+
35
+ const contentType = response.headers.get("content-type") ?? "";
36
+ const responseBody = contentType.includes("application/json")
37
+ ? await response.json()
38
+ : await response.text();
39
+
40
+ return NextResponse.json({
41
+ status: response.status,
42
+ statusText: response.statusText,
43
+ body: responseBody,
44
+ }, { status: response.status });
45
+ } catch (error) {
46
+ const message =
47
+ error instanceof Error
48
+ ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ""}`
49
+ : "Request failed";
50
+ return NextResponse.json(
51
+ {
52
+ status: 502,
53
+ statusText: "Bad Gateway",
54
+ body: `Could not reach ${url}\n${message}`,
55
+ },
56
+ { status: 502 },
57
+ );
58
+ }
59
+ }
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return Response.json({ status: 'ok' })
3
+ }
@@ -0,0 +1,90 @@
1
+ import { source } from '@/lib/source'
2
+ import { createSearchAPI, type AdvancedIndex } from 'fumadocs-core/search/server'
3
+ import type { StructuredData } from 'fumadocs-core/mdx-plugins'
4
+ import type { OpenAPIV3 } from 'openapi-types'
5
+ import { loadConfig } from '@/lib/config'
6
+ import { loadApiSpecs, type ApiSpec } from '@/lib/openapi'
7
+ import { getSpecSlug } from '@/lib/api-routes'
8
+
9
+ interface PageData {
10
+ title?: string
11
+ description?: string
12
+ structuredData?: StructuredData
13
+ load?: () => Promise<{ structuredData?: StructuredData }>
14
+ }
15
+
16
+ const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const
17
+ type HttpMethod = (typeof HTTP_METHODS)[number]
18
+
19
+ function getParamNames(op: OpenAPIV3.OperationObject): string[] {
20
+ const params = (op.parameters as OpenAPIV3.ParameterObject[] | undefined) ?? []
21
+ return params.map((p) => p.name)
22
+ }
23
+
24
+ function buildStructuredData(op: OpenAPIV3.OperationObject, method: string, pathStr: string) {
25
+ return {
26
+ headings: [{ id: op.operationId!, content: `${method.toUpperCase()} ${pathStr}` }],
27
+ contents: [{ heading: op.operationId!, content: `${method.toUpperCase()} ${[op.description, ...getParamNames(op)].filter(Boolean).join(' ')}` }],
28
+ }
29
+ }
30
+
31
+ function operationToIndex(specSlug: string, pathStr: string, method: HttpMethod, op: OpenAPIV3.OperationObject): AdvancedIndex {
32
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId!)}`
33
+ return {
34
+ id: url,
35
+ url,
36
+ title: `${method.toUpperCase()} ${op.summary ?? op.operationId!}`,
37
+ description: op.description ?? '',
38
+ structuredData: buildStructuredData(op, method, pathStr),
39
+ }
40
+ }
41
+
42
+ function pathEntryToIndexes(specSlug: string) {
43
+ return ([pathStr, pathItem]: [string, OpenAPIV3.PathItemObject | undefined]): AdvancedIndex[] => {
44
+ if (!pathItem) return []
45
+ const hasOp = (m: HttpMethod) => !!pathItem[m]?.operationId
46
+ const toIndex = (m: HttpMethod) => operationToIndex(specSlug, pathStr, m, pathItem[m]!)
47
+ return HTTP_METHODS.filter(hasOp).map(toIndex)
48
+ }
49
+ }
50
+
51
+ function specToIndexes(spec: ApiSpec): AdvancedIndex[] {
52
+ const specSlug = getSpecSlug(spec)
53
+ return Object.entries(spec.document.paths ?? {}).flatMap(pathEntryToIndexes(specSlug))
54
+ }
55
+
56
+ function buildApiIndexes(): AdvancedIndex[] {
57
+ const config = loadConfig()
58
+ if (!config.api?.length) return []
59
+ return loadApiSpecs(config.api).flatMap(specToIndexes)
60
+ }
61
+
62
+ export const { GET } = createSearchAPI('advanced', {
63
+ indexes: async (): Promise<AdvancedIndex[]> => {
64
+ const pages = source.getPages()
65
+ const indexes = await Promise.all(
66
+ pages.map(async (page): Promise<AdvancedIndex> => {
67
+ const data = page.data as PageData
68
+ let structuredData: StructuredData | undefined = data.structuredData
69
+
70
+ if (!structuredData && data.load) {
71
+ try {
72
+ const loaded = await data.load()
73
+ structuredData = loaded.structuredData
74
+ } catch (error) {
75
+ console.error(`Failed to load structured data for ${page.url}:`, error)
76
+ }
77
+ }
78
+
79
+ return {
80
+ id: page.url,
81
+ url: page.url,
82
+ title: data.title ?? '',
83
+ description: data.description ?? '',
84
+ structuredData: structuredData ?? { headings: [], contents: [] },
85
+ }
86
+ })
87
+ )
88
+ return [...indexes, ...buildApiIndexes()]
89
+ },
90
+ })
@@ -0,0 +1,22 @@
1
+ .layout {
2
+ height: 100vh;
3
+ overflow: hidden;
4
+ }
5
+
6
+ .body {
7
+ overflow: hidden;
8
+ }
9
+
10
+ .sidebar {
11
+ height: 100%;
12
+ }
13
+
14
+ .content {
15
+ height: 100%;
16
+ overflow-y: auto;
17
+ padding-right: 0;
18
+ }
19
+
20
+ .hiddenSearch {
21
+ display: none;
22
+ }
@@ -0,0 +1,26 @@
1
+ import { cx } from 'class-variance-authority'
2
+ import { loadConfig } from '@/lib/config'
3
+ import { loadApiSpecs } from '@/lib/openapi'
4
+ import { buildApiPageTree } from '@/lib/api-routes'
5
+ import { getTheme } from '@/themes/registry'
6
+ import { Search } from '@/components/ui/search'
7
+ import styles from './layout.module.css'
8
+
9
+ export default function ApiLayout({ children }: { children: React.ReactNode }) {
10
+ const config = loadConfig()
11
+ const { Layout, className } = getTheme(config.theme?.name)
12
+ const specs = loadApiSpecs(config.api ?? [])
13
+ const tree = buildApiPageTree(specs)
14
+
15
+ return (
16
+ <Layout config={config} tree={tree} classNames={{
17
+ layout: cx(styles.layout, className),
18
+ body: styles.body,
19
+ sidebar: styles.sidebar,
20
+ content: styles.content,
21
+ }}>
22
+ <Search className={styles.hiddenSearch} />
23
+ {children}
24
+ </Layout>
25
+ )
26
+ }
@@ -0,0 +1,57 @@
1
+ import { notFound } from 'next/navigation'
2
+ import type { OpenAPIV3 } from 'openapi-types'
3
+ import { Flex, Headline, Text } from '@raystack/apsara'
4
+ import { loadConfig } from '@/lib/config'
5
+ import { loadApiSpecs } from '@/lib/openapi'
6
+ import { buildApiRoutes, findApiOperation } from '@/lib/api-routes'
7
+ import { EndpointPage } from '@/components/api'
8
+
9
+ interface PageProps {
10
+ params: Promise<{ slug?: string[] }>
11
+ }
12
+
13
+ export default async function ApiPage({ params }: PageProps) {
14
+ const { slug } = await params
15
+ const config = loadConfig()
16
+ const specs = loadApiSpecs(config.api ?? [])
17
+
18
+ if (!slug || slug.length === 0) {
19
+ return <ApiLanding specs={specs} />
20
+ }
21
+
22
+ const match = findApiOperation(specs, slug)
23
+ if (!match) notFound()
24
+
25
+ return (
26
+ <EndpointPage
27
+ method={match.method}
28
+ path={match.path}
29
+ operation={match.operation}
30
+ serverUrl={match.spec.server.url}
31
+ specName={match.spec.name}
32
+ auth={match.spec.auth}
33
+ />
34
+ )
35
+ }
36
+
37
+ function ApiLanding({ specs }: { specs: { name: string; document: OpenAPIV3.Document }[] }) {
38
+ return (
39
+ <Flex direction="column" gap="large" style={{ padding: 'var(--rs-space-7)' }}>
40
+ <Headline size="medium" as="h1">API Reference</Headline>
41
+ {specs.map((spec) => (
42
+ <Flex key={spec.name} direction="column" gap="small">
43
+ <Headline size="small" as="h2">{spec.name}</Headline>
44
+ {spec.document.info.description && (
45
+ <Text size={3}>{spec.document.info.description}</Text>
46
+ )}
47
+ </Flex>
48
+ ))}
49
+ </Flex>
50
+ )
51
+ }
52
+
53
+ export function generateStaticParams() {
54
+ const config = loadConfig()
55
+ const specs = loadApiSpecs(config.api ?? [])
56
+ return [{ slug: [] }, ...buildApiRoutes(specs)]
57
+ }
@@ -0,0 +1,26 @@
1
+ import '@raystack/apsara/normalize.css'
2
+ import '@raystack/apsara/style.css'
3
+ import type { Metadata } from 'next'
4
+ import { loadConfig } from '@/lib/config'
5
+ import { Providers } from './providers'
6
+
7
+ const config = loadConfig()
8
+
9
+ export const metadata: Metadata = {
10
+ title: config.title,
11
+ description: config.description,
12
+ }
13
+
14
+ export default function RootLayout({
15
+ children,
16
+ }: {
17
+ children: React.ReactNode
18
+ }) {
19
+ return (
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body suppressHydrationWarning>
22
+ <Providers>{children}</Providers>
23
+ </body>
24
+ </html>
25
+ )
26
+ }
@@ -0,0 +1,8 @@
1
+ 'use client'
2
+
3
+ import { ThemeProvider } from '@raystack/apsara'
4
+ import type { ReactNode } from 'react'
5
+
6
+ export function Providers({ children }: { children: ReactNode }) {
7
+ return <ThemeProvider enableSystem>{children}</ThemeProvider>
8
+ }
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander'
2
+ import { spawn } from 'child_process'
3
+ import path from 'path'
4
+ import chalk from 'chalk'
5
+ import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
6
+
7
+ declare const PACKAGE_ROOT: string
8
+
9
+ const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
10
+
11
+ export const buildCommand = new Command('build')
12
+ .description('Build for production')
13
+ .option('-c, --content <path>', 'Content directory')
14
+ .action((options) => {
15
+ const contentDir = resolveContentDir(options.content)
16
+ loadCLIConfig(contentDir)
17
+
18
+ console.log(chalk.cyan('Building for production...'))
19
+ console.log(chalk.gray(`Content: ${contentDir}`))
20
+
21
+ const child = spawn(nextBin, ['build'], {
22
+ stdio: 'inherit',
23
+ cwd: PACKAGE_ROOT,
24
+ env: {
25
+ ...process.env,
26
+ CHRONICLE_CONTENT_DIR: contentDir,
27
+ },
28
+ })
29
+
30
+ attachLifecycleHandlers(child)
31
+ })
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander'
2
+ import { spawn } from 'child_process'
3
+ import path from 'path'
4
+ import chalk from 'chalk'
5
+ import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
6
+
7
+ declare const PACKAGE_ROOT: string
8
+
9
+ const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
10
+
11
+ export const devCommand = new Command('dev')
12
+ .description('Start development server')
13
+ .option('-p, --port <port>', 'Port number', '3000')
14
+ .option('-c, --content <path>', 'Content directory')
15
+ .action((options) => {
16
+ const contentDir = resolveContentDir(options.content)
17
+ loadCLIConfig(contentDir)
18
+
19
+ console.log(chalk.cyan('Starting dev server...'))
20
+ console.log(chalk.gray(`Content: ${contentDir}`))
21
+
22
+ const child = spawn(nextBin, ['dev', '-p', options.port], {
23
+ stdio: 'inherit',
24
+ cwd: PACKAGE_ROOT,
25
+ env: {
26
+ ...process.env,
27
+ CHRONICLE_CONTENT_DIR: contentDir,
28
+ },
29
+ })
30
+
31
+ attachLifecycleHandlers(child)
32
+ })
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import chalk from 'chalk'
5
+ import { stringify } from 'yaml'
6
+ import type { ChronicleConfig } from '@/types'
7
+
8
+ function createConfig(): ChronicleConfig {
9
+ return {
10
+ title: 'My Documentation',
11
+ description: 'Documentation powered by Chronicle',
12
+ theme: { name: 'default' },
13
+ search: { enabled: true, placeholder: 'Search documentation...' },
14
+ }
15
+ }
16
+
17
+ const sampleMdx = `---
18
+ title: Welcome
19
+ description: Getting started with your documentation
20
+ order: 1
21
+ ---
22
+
23
+ # Welcome
24
+
25
+ This is your documentation home page.
26
+ `
27
+
28
+ export const initCommand = new Command('init')
29
+ .description('Initialize a new Chronicle project')
30
+ .option('-d, --dir <path>', 'Content directory', '.')
31
+ .action((options) => {
32
+ const contentDir = path.resolve(options.dir)
33
+
34
+ // Create content directory
35
+ if (!fs.existsSync(contentDir)) {
36
+ fs.mkdirSync(contentDir, { recursive: true })
37
+ console.log(chalk.green('✓'), 'Created', contentDir)
38
+ }
39
+
40
+ // Create chronicle.yaml
41
+ const configPath = path.join(contentDir, 'chronicle.yaml')
42
+ if (!fs.existsSync(configPath)) {
43
+ fs.writeFileSync(configPath, stringify(createConfig()))
44
+ console.log(chalk.green('✓'), 'Created', configPath)
45
+ } else {
46
+ console.log(chalk.yellow('⚠'), configPath, 'already exists')
47
+ }
48
+
49
+ // Create sample index.mdx
50
+ const indexPath = path.join(contentDir, 'index.mdx')
51
+ if (!fs.existsSync(indexPath)) {
52
+ fs.writeFileSync(indexPath, sampleMdx)
53
+ console.log(chalk.green('✓'), 'Created', indexPath)
54
+ }
55
+
56
+ console.log(chalk.green('\n✓ Chronicle initialized!'))
57
+ console.log('\nRun', chalk.cyan('chronicle dev'), 'to start development server')
58
+ })
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander'
2
+ import { spawn } from 'child_process'
3
+ import path from 'path'
4
+ import chalk from 'chalk'
5
+ import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
6
+
7
+ declare const PACKAGE_ROOT: string
8
+
9
+ const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
10
+
11
+ export const serveCommand = new Command('serve')
12
+ .description('Build and start production server')
13
+ .option('-p, --port <port>', 'Port number', '3000')
14
+ .option('-c, --content <path>', 'Content directory')
15
+ .action((options) => {
16
+ const contentDir = resolveContentDir(options.content)
17
+ loadCLIConfig(contentDir)
18
+
19
+ const env = {
20
+ ...process.env,
21
+ CHRONICLE_CONTENT_DIR: contentDir,
22
+ }
23
+
24
+ console.log(chalk.cyan('Building for production...'))
25
+ console.log(chalk.gray(`Content: ${contentDir}`))
26
+
27
+ const buildChild = spawn(nextBin, ['build'], {
28
+ stdio: 'inherit',
29
+ cwd: PACKAGE_ROOT,
30
+ env,
31
+ })
32
+
33
+ process.once('SIGINT', () => buildChild.kill('SIGINT'))
34
+ process.once('SIGTERM', () => buildChild.kill('SIGTERM'))
35
+
36
+ buildChild.on('close', (code) => {
37
+ if (code !== 0) {
38
+ console.log(chalk.red('Build failed'))
39
+ process.exit(code ?? 1)
40
+ }
41
+
42
+ console.log(chalk.cyan('Starting production server...'))
43
+
44
+ const startChild = spawn(nextBin, ['start', '-p', options.port], {
45
+ stdio: 'inherit',
46
+ cwd: PACKAGE_ROOT,
47
+ env,
48
+ })
49
+
50
+ attachLifecycleHandlers(startChild)
51
+ })
52
+ })