@raystack/chronicle 0.1.3 → 0.3.0
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/cli/index.js +335 -89
- package/package.json +4 -3
- package/source.config.ts +1 -0
- package/src/app/[[...slug]]/page.tsx +61 -12
- package/src/app/apis/[[...slug]]/page.tsx +60 -0
- package/src/app/layout.tsx +32 -1
- package/src/app/og/route.tsx +62 -0
- package/src/app/robots.ts +10 -0
- package/src/app/sitemap.ts +29 -0
- package/src/cli/commands/build.ts +20 -13
- package/src/cli/commands/dev.ts +19 -12
- package/src/cli/commands/init.ts +114 -9
- package/src/cli/commands/serve.ts +21 -14
- package/src/cli/commands/start.ts +19 -12
- package/src/cli/utils/config.ts +12 -4
- package/src/cli/utils/index.ts +1 -0
- package/src/cli/utils/resolve.ts +6 -0
- package/src/cli/utils/scaffold.ts +137 -0
- package/src/components/ui/search.module.css +7 -0
- package/src/components/ui/search.tsx +7 -3
- package/src/lib/config.ts +22 -3
- package/src/lib/source.ts +1 -1
- package/src/types/config.ts +11 -0
- package/src/types/content.ts +1 -0
- package/tsconfig.json +30 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Metadata, ResolvingMetadata } from 'next'
|
|
1
2
|
import { notFound } from 'next/navigation'
|
|
2
3
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
3
4
|
import { Flex, Headline, Text } from '@raystack/apsara'
|
|
@@ -10,6 +11,65 @@ interface PageProps {
|
|
|
10
11
|
params: Promise<{ slug?: string[] }>
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export async function generateMetadata(
|
|
15
|
+
{ params }: PageProps,
|
|
16
|
+
parent: ResolvingMetadata,
|
|
17
|
+
): Promise<Metadata> {
|
|
18
|
+
const { slug } = await params
|
|
19
|
+
const config = loadConfig()
|
|
20
|
+
const specs = loadApiSpecs(config.api ?? [])
|
|
21
|
+
const parentMetadata = await parent
|
|
22
|
+
|
|
23
|
+
if (!slug || slug.length === 0) {
|
|
24
|
+
const apiDescription = `API documentation for ${config.title}`
|
|
25
|
+
const metadata: Metadata = {
|
|
26
|
+
title: 'API Reference',
|
|
27
|
+
description: apiDescription,
|
|
28
|
+
}
|
|
29
|
+
if (config.url) {
|
|
30
|
+
metadata.openGraph = {
|
|
31
|
+
...parentMetadata.openGraph,
|
|
32
|
+
title: 'API Reference',
|
|
33
|
+
description: apiDescription,
|
|
34
|
+
images: [{ url: `/og?title=${encodeURIComponent('API Reference')}&description=${encodeURIComponent(apiDescription)}`, width: 1200, height: 630 }],
|
|
35
|
+
}
|
|
36
|
+
metadata.twitter = {
|
|
37
|
+
...parentMetadata.twitter,
|
|
38
|
+
title: 'API Reference',
|
|
39
|
+
description: apiDescription,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return metadata
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const match = findApiOperation(specs, slug)
|
|
46
|
+
if (!match) return {}
|
|
47
|
+
|
|
48
|
+
const operation = match.operation as OpenAPIV3.OperationObject
|
|
49
|
+
const title = operation.summary ?? `${match.method.toUpperCase()} ${match.path}`
|
|
50
|
+
const description = operation.description
|
|
51
|
+
|
|
52
|
+
const metadata: Metadata = { title, description }
|
|
53
|
+
|
|
54
|
+
if (config.url) {
|
|
55
|
+
const ogParams = new URLSearchParams({ title })
|
|
56
|
+
if (description) ogParams.set('description', description)
|
|
57
|
+
metadata.openGraph = {
|
|
58
|
+
...parentMetadata.openGraph,
|
|
59
|
+
title,
|
|
60
|
+
description,
|
|
61
|
+
images: [{ url: `/og?${ogParams.toString()}`, width: 1200, height: 630 }],
|
|
62
|
+
}
|
|
63
|
+
metadata.twitter = {
|
|
64
|
+
...parentMetadata.twitter,
|
|
65
|
+
title,
|
|
66
|
+
description,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return metadata
|
|
71
|
+
}
|
|
72
|
+
|
|
13
73
|
export default async function ApiPage({ params }: PageProps) {
|
|
14
74
|
const { slug } = await params
|
|
15
75
|
const config = loadConfig()
|
package/src/app/layout.tsx
CHANGED
|
@@ -7,8 +7,28 @@ import { Providers } from './providers'
|
|
|
7
7
|
const config = loadConfig()
|
|
8
8
|
|
|
9
9
|
export const metadata: Metadata = {
|
|
10
|
-
title:
|
|
10
|
+
title: {
|
|
11
|
+
default: config.title,
|
|
12
|
+
template: `%s | ${config.title}`,
|
|
13
|
+
},
|
|
11
14
|
description: config.description,
|
|
15
|
+
...(config.url && {
|
|
16
|
+
metadataBase: new URL(config.url),
|
|
17
|
+
openGraph: {
|
|
18
|
+
title: config.title,
|
|
19
|
+
description: config.description,
|
|
20
|
+
url: config.url,
|
|
21
|
+
siteName: config.title,
|
|
22
|
+
type: 'website',
|
|
23
|
+
images: [{ url: '/og?title=' + encodeURIComponent(config.title), width: 1200, height: 630 }],
|
|
24
|
+
},
|
|
25
|
+
twitter: {
|
|
26
|
+
card: 'summary_large_image',
|
|
27
|
+
title: config.title,
|
|
28
|
+
description: config.description,
|
|
29
|
+
images: ['/og?title=' + encodeURIComponent(config.title)],
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
12
32
|
}
|
|
13
33
|
|
|
14
34
|
export default function RootLayout({
|
|
@@ -19,6 +39,17 @@ export default function RootLayout({
|
|
|
19
39
|
return (
|
|
20
40
|
<html lang="en" suppressHydrationWarning>
|
|
21
41
|
<body suppressHydrationWarning>
|
|
42
|
+
{config.url && (
|
|
43
|
+
<script type="application/ld+json">
|
|
44
|
+
{JSON.stringify({
|
|
45
|
+
'@context': 'https://schema.org',
|
|
46
|
+
'@type': 'WebSite',
|
|
47
|
+
name: config.title,
|
|
48
|
+
description: config.description,
|
|
49
|
+
url: config.url,
|
|
50
|
+
}, null, 2)}
|
|
51
|
+
</script>
|
|
52
|
+
)}
|
|
22
53
|
<Providers>{children}</Providers>
|
|
23
54
|
</body>
|
|
24
55
|
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ImageResponse } from 'next/og'
|
|
2
|
+
import type { NextRequest } from 'next/server'
|
|
3
|
+
import { loadConfig } from '@/lib/config'
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
const { searchParams } = request.nextUrl
|
|
7
|
+
const title = searchParams.get('title') ?? loadConfig().title
|
|
8
|
+
const description = searchParams.get('description') ?? ''
|
|
9
|
+
const siteName = loadConfig().title
|
|
10
|
+
|
|
11
|
+
return new ImageResponse(
|
|
12
|
+
(
|
|
13
|
+
<div
|
|
14
|
+
style={{
|
|
15
|
+
height: '100%',
|
|
16
|
+
width: '100%',
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: 'column',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
padding: '60px 80px',
|
|
21
|
+
backgroundColor: '#0a0a0a',
|
|
22
|
+
color: '#fafafa',
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
style={{
|
|
27
|
+
fontSize: 24,
|
|
28
|
+
color: '#888',
|
|
29
|
+
marginBottom: 16,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{siteName}
|
|
33
|
+
</div>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
fontSize: 56,
|
|
37
|
+
fontWeight: 700,
|
|
38
|
+
lineHeight: 1.2,
|
|
39
|
+
marginBottom: 24,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{title}
|
|
43
|
+
</div>
|
|
44
|
+
{description && (
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
fontSize: 24,
|
|
48
|
+
color: '#999',
|
|
49
|
+
lineHeight: 1.4,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{description}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
{
|
|
58
|
+
width: 1200,
|
|
59
|
+
height: 630,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next'
|
|
2
|
+
import { loadConfig } from '@/lib/config'
|
|
3
|
+
|
|
4
|
+
export default function robots(): MetadataRoute.Robots {
|
|
5
|
+
const config = loadConfig()
|
|
6
|
+
return {
|
|
7
|
+
rules: { userAgent: '*', allow: '/' },
|
|
8
|
+
...(config.url && { sitemap: `${config.url}/sitemap.xml` }),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next'
|
|
2
|
+
import { loadConfig } from '@/lib/config'
|
|
3
|
+
import { source } from '@/lib/source'
|
|
4
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
5
|
+
import { buildApiRoutes } from '@/lib/api-routes'
|
|
6
|
+
|
|
7
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
8
|
+
const config = loadConfig()
|
|
9
|
+
if (!config.url) return []
|
|
10
|
+
|
|
11
|
+
const baseUrl = config.url.replace(/\/$/, '')
|
|
12
|
+
|
|
13
|
+
const docPages = source.getPages().map((page) => ({
|
|
14
|
+
url: `${baseUrl}/${page.slugs.join('/')}`,
|
|
15
|
+
...(page.data.lastModified && { lastModified: new Date(page.data.lastModified) }),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const apiPages = config.api?.length
|
|
19
|
+
? buildApiRoutes(loadApiSpecs(config.api)).map((route) => ({
|
|
20
|
+
url: `${baseUrl}/apis/${route.slug.join('/')}`,
|
|
21
|
+
}))
|
|
22
|
+
: []
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{ url: baseUrl },
|
|
26
|
+
...docPages,
|
|
27
|
+
...apiPages,
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import
|
|
4
|
+
import fs from 'fs'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
9
|
-
const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
|
|
6
|
+
import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
|
|
10
7
|
|
|
11
8
|
export const buildCommand = new Command('build')
|
|
12
9
|
.description('Build for production')
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
.action(() => {
|
|
11
|
+
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
12
|
+
if (!fs.existsSync(scaffoldPath)) {
|
|
13
|
+
console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let nextCli: string
|
|
18
|
+
try {
|
|
19
|
+
nextCli = resolveNextCli()
|
|
20
|
+
} catch {
|
|
21
|
+
console.log(chalk.red('Error: Next.js CLI not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
17
24
|
|
|
18
25
|
console.log(chalk.cyan('Building for production...'))
|
|
19
|
-
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
20
26
|
|
|
21
|
-
const child = spawn(
|
|
27
|
+
const child = spawn(process.execPath, [nextCli, 'build'], {
|
|
22
28
|
stdio: 'inherit',
|
|
23
|
-
cwd:
|
|
29
|
+
cwd: scaffoldPath,
|
|
24
30
|
env: {
|
|
25
31
|
...process.env,
|
|
26
|
-
|
|
32
|
+
CHRONICLE_PROJECT_ROOT: process.cwd(),
|
|
33
|
+
CHRONICLE_CONTENT_DIR: './content',
|
|
27
34
|
},
|
|
28
35
|
})
|
|
29
36
|
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import
|
|
4
|
+
import fs from 'fs'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
9
|
-
const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
|
|
6
|
+
import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
|
|
10
7
|
|
|
11
8
|
export const devCommand = new Command('dev')
|
|
12
9
|
.description('Start development server')
|
|
13
10
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
14
|
-
.option('-c, --content <path>', 'Content directory')
|
|
15
11
|
.action((options) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
12
|
+
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
13
|
+
if (!fs.existsSync(scaffoldPath)) {
|
|
14
|
+
console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let nextCli: string
|
|
19
|
+
try {
|
|
20
|
+
nextCli = resolveNextCli()
|
|
21
|
+
} catch {
|
|
22
|
+
console.log(chalk.red('Error: Next.js CLI not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
console.log(chalk.cyan('Starting dev server...'))
|
|
20
|
-
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
21
27
|
|
|
22
|
-
const child = spawn(
|
|
28
|
+
const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], {
|
|
23
29
|
stdio: 'inherit',
|
|
24
|
-
cwd:
|
|
30
|
+
cwd: scaffoldPath,
|
|
25
31
|
env: {
|
|
26
32
|
...process.env,
|
|
27
|
-
|
|
33
|
+
CHRONICLE_PROJECT_ROOT: process.cwd(),
|
|
34
|
+
CHRONICLE_CONTENT_DIR: './content',
|
|
28
35
|
},
|
|
29
36
|
})
|
|
30
37
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
2
3
|
import fs from 'fs'
|
|
3
4
|
import path from 'path'
|
|
4
5
|
import chalk from 'chalk'
|
|
5
6
|
import { stringify } from 'yaml'
|
|
6
7
|
import type { ChronicleConfig } from '@/types'
|
|
8
|
+
import { loadCLIConfig, scaffoldDir, detectPackageManager, getChronicleVersion } from '@/cli/utils'
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
function createConfig(): ChronicleConfig {
|
|
9
12
|
return {
|
|
@@ -14,6 +17,29 @@ function createConfig(): ChronicleConfig {
|
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
function createPackageJson(name: string): Record<string, unknown> {
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
private: true,
|
|
24
|
+
type: 'module',
|
|
25
|
+
scripts: {
|
|
26
|
+
dev: 'chronicle dev',
|
|
27
|
+
build: 'chronicle build',
|
|
28
|
+
start: 'chronicle start',
|
|
29
|
+
},
|
|
30
|
+
dependencies: {
|
|
31
|
+
'@raystack/chronicle': `^${getChronicleVersion()}`,
|
|
32
|
+
},
|
|
33
|
+
devDependencies: {
|
|
34
|
+
'@raystack/tools-config': '0.56.0',
|
|
35
|
+
'openapi-types': '^12.1.3',
|
|
36
|
+
typescript: '5.9.3',
|
|
37
|
+
'@types/react': '^19.2.10',
|
|
38
|
+
'@types/node': '^25.1.0',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
const sampleMdx = `---
|
|
18
44
|
title: Welcome
|
|
19
45
|
description: Getting started with your documentation
|
|
@@ -27,18 +53,71 @@ This is your documentation home page.
|
|
|
27
53
|
|
|
28
54
|
export const initCommand = new Command('init')
|
|
29
55
|
.description('Initialize a new Chronicle project')
|
|
30
|
-
.option('-
|
|
56
|
+
.option('-c, --content <path>', 'Content directory name', 'content')
|
|
31
57
|
.action((options) => {
|
|
32
|
-
const
|
|
58
|
+
const projectDir = process.cwd()
|
|
59
|
+
const dirName = path.basename(projectDir) || 'docs'
|
|
60
|
+
const contentDir = path.join(projectDir, options.content)
|
|
33
61
|
|
|
34
|
-
// Create content directory
|
|
62
|
+
// Create content directory if it doesn't exist
|
|
35
63
|
if (!fs.existsSync(contentDir)) {
|
|
36
64
|
fs.mkdirSync(contentDir, { recursive: true })
|
|
37
65
|
console.log(chalk.green('✓'), 'Created', contentDir)
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
// Create
|
|
41
|
-
const
|
|
68
|
+
// Create or update package.json in project root
|
|
69
|
+
const packageJsonPath = path.join(projectDir, 'package.json')
|
|
70
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
71
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(createPackageJson(dirName), null, 2) + '\n')
|
|
72
|
+
console.log(chalk.green('✓'), 'Created', packageJsonPath)
|
|
73
|
+
} else {
|
|
74
|
+
const existing = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
75
|
+
const template = createPackageJson(dirName)
|
|
76
|
+
let updated = false
|
|
77
|
+
|
|
78
|
+
// Set type to module
|
|
79
|
+
if (existing.type !== 'module') {
|
|
80
|
+
existing.type = 'module'
|
|
81
|
+
updated = true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge missing scripts
|
|
85
|
+
if (!existing.scripts) existing.scripts = {}
|
|
86
|
+
for (const [key, value] of Object.entries(template.scripts as Record<string, string>)) {
|
|
87
|
+
if (!existing.scripts[key]) {
|
|
88
|
+
existing.scripts[key] = value
|
|
89
|
+
updated = true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Merge missing dependencies
|
|
94
|
+
if (!existing.dependencies) existing.dependencies = {}
|
|
95
|
+
for (const [key, value] of Object.entries(template.dependencies as Record<string, string>)) {
|
|
96
|
+
if (!existing.dependencies[key]) {
|
|
97
|
+
existing.dependencies[key] = value
|
|
98
|
+
updated = true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Merge missing devDependencies
|
|
103
|
+
if (!existing.devDependencies) existing.devDependencies = {}
|
|
104
|
+
for (const [key, value] of Object.entries(template.devDependencies as Record<string, string>)) {
|
|
105
|
+
if (!existing.devDependencies[key]) {
|
|
106
|
+
existing.devDependencies[key] = value
|
|
107
|
+
updated = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (updated) {
|
|
112
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(existing, null, 2) + '\n')
|
|
113
|
+
console.log(chalk.green('✓'), 'Updated', packageJsonPath, 'with missing scripts/deps')
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.yellow('⚠'), packageJsonPath, 'already has all required entries')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create chronicle.yaml in project root
|
|
120
|
+
const configPath = path.join(projectDir, 'chronicle.yaml')
|
|
42
121
|
if (!fs.existsSync(configPath)) {
|
|
43
122
|
fs.writeFileSync(configPath, stringify(createConfig()))
|
|
44
123
|
console.log(chalk.green('✓'), 'Created', configPath)
|
|
@@ -46,13 +125,39 @@ export const initCommand = new Command('init')
|
|
|
46
125
|
console.log(chalk.yellow('⚠'), configPath, 'already exists')
|
|
47
126
|
}
|
|
48
127
|
|
|
49
|
-
// Create sample index.mdx
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
128
|
+
// Create sample index.mdx only if content dir is empty
|
|
129
|
+
const contentFiles = fs.readdirSync(contentDir)
|
|
130
|
+
if (contentFiles.length === 0) {
|
|
131
|
+
const indexPath = path.join(contentDir, 'index.mdx')
|
|
52
132
|
fs.writeFileSync(indexPath, sampleMdx)
|
|
53
133
|
console.log(chalk.green('✓'), 'Created', indexPath)
|
|
54
134
|
}
|
|
55
135
|
|
|
136
|
+
// Add entries to .gitignore
|
|
137
|
+
const gitignorePath = path.join(projectDir, '.gitignore')
|
|
138
|
+
const gitignoreEntries = ['.chronicle', 'node_modules', '.next']
|
|
139
|
+
if (fs.existsSync(gitignorePath)) {
|
|
140
|
+
const existing = fs.readFileSync(gitignorePath, 'utf-8')
|
|
141
|
+
const missing = gitignoreEntries.filter(e => !existing.includes(e))
|
|
142
|
+
if (missing.length > 0) {
|
|
143
|
+
fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`)
|
|
144
|
+
console.log(chalk.green('✓'), 'Added', missing.join(', '), 'to .gitignore')
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
fs.writeFileSync(gitignorePath, `${gitignoreEntries.join('\n')}\n`)
|
|
148
|
+
console.log(chalk.green('✓'), 'Created .gitignore')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Install dependencies
|
|
152
|
+
const pm = detectPackageManager()
|
|
153
|
+
console.log(chalk.cyan(`\nInstalling dependencies with ${pm}...`))
|
|
154
|
+
execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' })
|
|
155
|
+
|
|
156
|
+
// Scaffold .chronicle/ directory
|
|
157
|
+
loadCLIConfig(contentDir)
|
|
158
|
+
scaffoldDir(contentDir)
|
|
159
|
+
|
|
160
|
+
const runCmd = pm === 'npm' ? 'npx' : pm === 'bun' ? 'bunx' : `${pm} dlx`
|
|
56
161
|
console.log(chalk.green('\n✓ Chronicle initialized!'))
|
|
57
|
-
console.log('\nRun', chalk.cyan(
|
|
162
|
+
console.log('\nRun', chalk.cyan(`${runCmd} chronicle dev`), 'to start development server')
|
|
58
163
|
})
|
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import
|
|
4
|
+
import fs from 'fs'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
9
|
-
const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
|
|
6
|
+
import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
|
|
10
7
|
|
|
11
8
|
export const serveCommand = new Command('serve')
|
|
12
9
|
.description('Build and start production server')
|
|
13
10
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
14
|
-
.option('-c, --content <path>', 'Content directory')
|
|
15
11
|
.action((options) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
12
|
+
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
13
|
+
if (!fs.existsSync(scaffoldPath)) {
|
|
14
|
+
console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let nextCli: string
|
|
19
|
+
try {
|
|
20
|
+
nextCli = resolveNextCli()
|
|
21
|
+
} catch {
|
|
22
|
+
console.log(chalk.red('Error: Next.js CLI not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
const env = {
|
|
20
27
|
...process.env,
|
|
21
|
-
|
|
28
|
+
CHRONICLE_PROJECT_ROOT: process.cwd(),
|
|
29
|
+
CHRONICLE_CONTENT_DIR: './content',
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
console.log(chalk.cyan('Building for production...'))
|
|
25
|
-
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
26
33
|
|
|
27
|
-
const buildChild = spawn(
|
|
34
|
+
const buildChild = spawn(process.execPath, [nextCli, 'build'], {
|
|
28
35
|
stdio: 'inherit',
|
|
29
|
-
cwd:
|
|
36
|
+
cwd: scaffoldPath,
|
|
30
37
|
env,
|
|
31
38
|
})
|
|
32
39
|
|
|
@@ -41,9 +48,9 @@ export const serveCommand = new Command('serve')
|
|
|
41
48
|
|
|
42
49
|
console.log(chalk.cyan('Starting production server...'))
|
|
43
50
|
|
|
44
|
-
const startChild = spawn(
|
|
51
|
+
const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
|
|
45
52
|
stdio: 'inherit',
|
|
46
|
-
cwd:
|
|
53
|
+
cwd: scaffoldPath,
|
|
47
54
|
env,
|
|
48
55
|
})
|
|
49
56
|
|
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import
|
|
4
|
+
import fs from 'fs'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
9
|
-
const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next')
|
|
6
|
+
import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
|
|
10
7
|
|
|
11
8
|
export const startCommand = new Command('start')
|
|
12
9
|
.description('Start production server')
|
|
13
10
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
14
|
-
.option('-c, --content <path>', 'Content directory')
|
|
15
11
|
.action((options) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
12
|
+
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
13
|
+
if (!fs.existsSync(scaffoldPath)) {
|
|
14
|
+
console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let nextCli: string
|
|
19
|
+
try {
|
|
20
|
+
nextCli = resolveNextCli()
|
|
21
|
+
} catch {
|
|
22
|
+
console.log(chalk.red('Error: Next.js CLI not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.'))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
console.log(chalk.cyan('Starting production server...'))
|
|
20
|
-
console.log(chalk.gray(`Content: ${contentDir}`))
|
|
21
27
|
|
|
22
|
-
const child = spawn(
|
|
28
|
+
const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
|
|
23
29
|
stdio: 'inherit',
|
|
24
|
-
cwd:
|
|
30
|
+
cwd: scaffoldPath,
|
|
25
31
|
env: {
|
|
26
32
|
...process.env,
|
|
27
|
-
|
|
33
|
+
CHRONICLE_PROJECT_ROOT: process.cwd(),
|
|
34
|
+
CHRONICLE_CONTENT_DIR: './content',
|
|
28
35
|
},
|
|
29
36
|
})
|
|
30
37
|
|
package/src/cli/utils/config.ts
CHANGED
|
@@ -13,14 +13,22 @@ export interface CLIConfig {
|
|
|
13
13
|
export function resolveContentDir(contentFlag?: string): string {
|
|
14
14
|
if (contentFlag) return path.resolve(contentFlag)
|
|
15
15
|
if (process.env.CHRONICLE_CONTENT_DIR) return path.resolve(process.env.CHRONICLE_CONTENT_DIR)
|
|
16
|
-
return
|
|
16
|
+
return path.resolve('content')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveConfigPath(contentDir: string): string | null {
|
|
20
|
+
const cwdPath = path.join(process.cwd(), 'chronicle.yaml')
|
|
21
|
+
if (fs.existsSync(cwdPath)) return cwdPath
|
|
22
|
+
const contentPath = path.join(contentDir, 'chronicle.yaml')
|
|
23
|
+
if (fs.existsSync(contentPath)) return contentPath
|
|
24
|
+
return null
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export function loadCLIConfig(contentDir: string): CLIConfig {
|
|
20
|
-
const configPath =
|
|
28
|
+
const configPath = resolveConfigPath(contentDir)
|
|
21
29
|
|
|
22
|
-
if (!
|
|
23
|
-
console.log(chalk.red(
|
|
30
|
+
if (!configPath) {
|
|
31
|
+
console.log(chalk.red(`Error: chronicle.yaml not found in '${process.cwd()}' or '${contentDir}'`))
|
|
24
32
|
console.log(chalk.gray(`Run 'chronicle init' to create one`))
|
|
25
33
|
process.exit(1)
|
|
26
34
|
}
|
package/src/cli/utils/index.ts
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
|
|
4
|
+
// After bundling: dist/cli/index.js → ../.. = package root
|
|
5
|
+
// After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
|
|
6
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|