@raystack/chronicle 0.1.0-canary.5a2be79 → 0.1.0-canary.5a730d4

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 (76) hide show
  1. package/dist/cli/index.js +391 -9834
  2. package/package.json +16 -11
  3. package/src/cli/__tests__/config.test.ts +25 -0
  4. package/src/cli/__tests__/scaffold.test.ts +10 -0
  5. package/src/cli/commands/build.ts +38 -18
  6. package/src/cli/commands/dev.ts +10 -22
  7. package/src/cli/commands/init.ts +28 -29
  8. package/src/cli/commands/serve.ts +38 -36
  9. package/src/cli/commands/start.ts +12 -21
  10. package/src/cli/utils/config.ts +1 -1
  11. package/src/cli/utils/index.ts +1 -2
  12. package/src/cli/utils/resolve.ts +2 -0
  13. package/src/cli/utils/scaffold.ts +4 -115
  14. package/src/components/mdx/code.tsx +10 -1
  15. package/src/components/mdx/details.module.css +1 -24
  16. package/src/components/mdx/details.tsx +2 -3
  17. package/src/components/mdx/image.tsx +5 -19
  18. package/src/components/mdx/index.tsx +3 -3
  19. package/src/components/mdx/link.tsx +10 -11
  20. package/src/components/ui/footer.tsx +3 -2
  21. package/src/components/ui/search.module.css +7 -0
  22. package/src/components/ui/search.tsx +58 -86
  23. package/src/lib/config.ts +1 -0
  24. package/src/lib/head.tsx +45 -0
  25. package/src/lib/page-context.tsx +95 -0
  26. package/src/lib/source.ts +92 -21
  27. package/src/{app/apis/[[...slug]]/layout.tsx → pages/ApiLayout.tsx} +10 -7
  28. package/src/pages/ApiPage.tsx +68 -0
  29. package/src/pages/DocsLayout.tsx +18 -0
  30. package/src/pages/DocsPage.tsx +43 -0
  31. package/src/pages/NotFound.tsx +10 -0
  32. package/src/pages/__tests__/head.test.tsx +57 -0
  33. package/src/server/App.tsx +59 -0
  34. package/src/server/__tests__/entry-server.test.tsx +35 -0
  35. package/src/server/__tests__/handlers.test.ts +77 -0
  36. package/src/server/__tests__/og.test.ts +23 -0
  37. package/src/server/__tests__/router.test.ts +72 -0
  38. package/src/server/__tests__/vite-config.test.ts +25 -0
  39. package/src/server/dev.ts +156 -0
  40. package/src/server/entry-client.tsx +74 -0
  41. package/src/server/entry-prod.ts +127 -0
  42. package/src/server/entry-server.tsx +35 -0
  43. package/src/server/handlers/apis-proxy.ts +52 -0
  44. package/src/{app/api/health/route.ts → server/handlers/health.ts} +1 -1
  45. package/src/server/handlers/llms.ts +58 -0
  46. package/src/server/handlers/og.ts +87 -0
  47. package/src/server/handlers/robots.ts +11 -0
  48. package/src/server/handlers/search.ts +140 -0
  49. package/src/server/handlers/sitemap.ts +39 -0
  50. package/src/server/handlers/specs.ts +9 -0
  51. package/src/server/index.html +12 -0
  52. package/src/server/prod.ts +18 -0
  53. package/src/server/router.ts +42 -0
  54. package/src/server/vite-config.ts +65 -0
  55. package/src/themes/default/Layout.tsx +9 -10
  56. package/src/themes/default/Page.module.css +56 -0
  57. package/src/themes/default/font.ts +4 -6
  58. package/src/themes/paper/ChapterNav.tsx +5 -6
  59. package/src/themes/paper/Page.tsx +8 -9
  60. package/src/types/config.ts +11 -0
  61. package/src/types/content.ts +1 -0
  62. package/tsconfig.json +2 -3
  63. package/next.config.mjs +0 -10
  64. package/source.config.ts +0 -50
  65. package/src/app/[[...slug]]/layout.tsx +0 -15
  66. package/src/app/[[...slug]]/page.tsx +0 -57
  67. package/src/app/api/apis-proxy/route.ts +0 -59
  68. package/src/app/api/search/route.ts +0 -90
  69. package/src/app/apis/[[...slug]]/page.tsx +0 -57
  70. package/src/app/layout.tsx +0 -26
  71. package/src/app/llms-full.txt/route.ts +0 -18
  72. package/src/app/llms.txt/route.ts +0 -15
  73. package/src/app/providers.tsx +0 -8
  74. package/src/cli/utils/process.ts +0 -7
  75. package/src/lib/get-llm-text.ts +0 -10
  76. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.1.0-canary.5a2be79",
3
+ "version": "0.1.0-canary.5a730d4",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,8 +9,6 @@
9
9
  "dist",
10
10
  "src",
11
11
  "templates",
12
- "next.config.mjs",
13
- "source.config.ts",
14
12
  "tsconfig.json"
15
13
  ],
16
14
  "bin": {
@@ -37,27 +35,34 @@
37
35
  "@codemirror/theme-one-dark": "^6.1.3",
38
36
  "@codemirror/view": "^6.39.14",
39
37
  "@heroicons/react": "^2.2.0",
38
+ "@mdx-js/rollup": "^3.1.1",
40
39
  "@raystack/apsara": "^0.56.0",
41
- "@types/unist": "^3.0.3",
40
+ "@vitejs/plugin-react": "^6.0.1",
42
41
  "chalk": "^5.6.2",
43
42
  "class-variance-authority": "^0.7.1",
44
43
  "codemirror": "^6.0.2",
45
44
  "commander": "^14.0.2",
46
- "fumadocs-core": "16.6.15",
47
- "fumadocs-mdx": "^14.2.6",
45
+ "glob": "^11.0.0",
46
+ "gray-matter": "^4.0.3",
48
47
  "lodash": "^4.17.23",
49
48
  "mermaid": "^11.13.0",
50
- "next": "16.1.6",
49
+ "openapi-types": "^12.1.3",
51
50
  "react": "^19.0.0",
52
51
  "react-device-detect": "^2.2.3",
53
52
  "react-dom": "^19.0.0",
54
- "remark-attr": "^0.11.1",
53
+ "react-router-dom": "^7.13.1",
55
54
  "remark-directive": "^4.0.0",
55
+ "remark-gfm": "^4.0.1",
56
+ "@shikijs/rehype": "^4.0.2",
57
+ "remark-frontmatter": "^5.0.0",
58
+ "remark-mdx-frontmatter": "^5.2.0",
59
+ "satori": "^0.25.0",
60
+ "sirv": "^3.0.1",
56
61
  "slugify": "^1.6.6",
57
62
  "unified": "^11.0.5",
58
63
  "unist-util-visit": "^5.1.0",
59
- "openapi-types": "^12.1.3",
60
- "yaml": "^2.8.2",
61
- "zod": "^4.3.6"
64
+ "minisearch": "^7.2.0",
65
+ "vite": "^8.0.0",
66
+ "yaml": "^2.8.2"
62
67
  }
63
68
  }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolveContentDir } from '../utils/config'
3
+
4
+ describe('resolveContentDir', () => {
5
+ it('returns flag value when provided', () => {
6
+ const result = resolveContentDir('/custom/path')
7
+ expect(result).toBe('/custom/path')
8
+ })
9
+
10
+ it('returns env var when set', () => {
11
+ const original = process.env.CHRONICLE_CONTENT_DIR
12
+ process.env.CHRONICLE_CONTENT_DIR = '/env/content'
13
+ const result = resolveContentDir()
14
+ expect(result).toContain('env/content')
15
+ process.env.CHRONICLE_CONTENT_DIR = original
16
+ })
17
+
18
+ it('defaults to content directory', () => {
19
+ const original = process.env.CHRONICLE_CONTENT_DIR
20
+ delete process.env.CHRONICLE_CONTENT_DIR
21
+ const result = resolveContentDir()
22
+ expect(result).toContain('content')
23
+ process.env.CHRONICLE_CONTENT_DIR = original
24
+ })
25
+ })
@@ -0,0 +1,10 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { detectPackageManager } from '../utils/scaffold'
3
+
4
+ describe('detectPackageManager', () => {
5
+ it('returns a string', () => {
6
+ const result = detectPackageManager()
7
+ expect(typeof result).toBe('string')
8
+ expect(['npm', 'bun', 'pnpm', 'yarn']).toContain(result)
9
+ })
10
+ })
@@ -1,32 +1,52 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import fs from 'fs'
5
3
  import chalk from 'chalk'
6
- import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
7
6
 
8
7
  export const buildCommand = new Command('build')
9
8
  .description('Build for production')
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
- }
9
+ .option('-c, --content <path>', 'Content directory')
10
+ .option('-o, --outDir <path>', 'Output directory', 'dist')
11
+ .action(async (options) => {
12
+ const contentDir = resolveContentDir(options.content)
13
+ const outDir = path.resolve(options.outDir)
16
14
 
17
- const nextCli = resolveNextCli()
15
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
16
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
18
17
 
19
18
  console.log(chalk.cyan('Building for production...'))
20
19
 
21
- const child = spawn(process.execPath, [nextCli, 'build'], {
22
- stdio: 'inherit',
23
- cwd: scaffoldPath,
24
- env: {
25
- ...process.env,
26
- CHRONICLE_PROJECT_ROOT: process.cwd(),
27
- CHRONICLE_CONTENT_DIR: './content',
20
+ const { build } = await import('vite')
21
+ const { createViteConfig } = await import('@/server/vite-config')
22
+
23
+ const baseConfig = await createViteConfig({ root: PACKAGE_ROOT, contentDir })
24
+
25
+ // Build client bundle
26
+ console.log(chalk.gray('Building client...'))
27
+ await build({
28
+ ...baseConfig,
29
+ build: {
30
+ outDir: path.join(outDir, 'client'),
31
+ ssrManifest: true,
32
+ rolldownOptions: {
33
+ input: path.resolve(PACKAGE_ROOT, 'src/server/index.html'),
34
+ },
35
+ },
36
+ })
37
+
38
+ // Build server bundle (noExternal: true to bundle all deps for portability)
39
+ console.log(chalk.gray('Building server...'))
40
+ await build({
41
+ ...baseConfig,
42
+ ssr: {
43
+ noExternal: true,
44
+ },
45
+ build: {
46
+ outDir: path.join(outDir, 'server'),
47
+ ssr: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts'),
28
48
  },
29
49
  })
30
50
 
31
- attachLifecycleHandlers(child)
51
+ console.log(chalk.green('Build complete →'), outDir)
32
52
  })
@@ -1,33 +1,21 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
- import path from 'path'
4
- import fs from 'fs'
5
2
  import chalk from 'chalk'
6
- import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
3
+ import { resolveContentDir } from '@/cli/utils/config'
4
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
7
5
 
8
6
  export const devCommand = new Command('dev')
9
7
  .description('Start development server')
10
8
  .option('-p, --port <port>', 'Port number', '3000')
11
- .action((options) => {
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
- }
9
+ .option('-c, --content <path>', 'Content directory')
10
+ .action(async (options) => {
11
+ const contentDir = resolveContentDir(options.content)
12
+ const port = parseInt(options.port, 10)
17
13
 
18
- const nextCli = resolveNextCli()
14
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
15
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
19
16
 
20
17
  console.log(chalk.cyan('Starting dev server...'))
21
18
 
22
- const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], {
23
- stdio: 'inherit',
24
- cwd: scaffoldPath,
25
- env: {
26
- ...process.env,
27
- CHRONICLE_PROJECT_ROOT: process.cwd(),
28
- CHRONICLE_CONTENT_DIR: './content',
29
- },
30
- })
31
-
32
- attachLifecycleHandlers(child)
19
+ const { startDevServer } = await import('@/server/dev')
20
+ await startDevServer({ port, root: PACKAGE_ROOT, contentDir })
33
21
  })
@@ -5,8 +5,7 @@ import path from 'path'
5
5
  import chalk from 'chalk'
6
6
  import { stringify } from 'yaml'
7
7
  import type { ChronicleConfig } from '@/types'
8
- import { loadCLIConfig, scaffoldDir, detectPackageManager } from '@/cli/utils'
9
-
8
+ import { detectPackageManager, getChronicleVersion } from '@/cli/utils/scaffold'
10
9
 
11
10
  function createConfig(): ChronicleConfig {
12
11
  return {
@@ -21,13 +20,14 @@ function createPackageJson(name: string): Record<string, unknown> {
21
20
  return {
22
21
  name,
23
22
  private: true,
23
+ type: 'module',
24
24
  scripts: {
25
25
  dev: 'chronicle dev',
26
26
  build: 'chronicle build',
27
27
  start: 'chronicle start',
28
28
  },
29
29
  dependencies: {
30
- '@raystack/chronicle': 'latest',
30
+ '@raystack/chronicle': `^${getChronicleVersion()}`,
31
31
  },
32
32
  devDependencies: {
33
33
  '@raystack/tools-config': '0.56.0',
@@ -58,23 +58,27 @@ export const initCommand = new Command('init')
58
58
  const dirName = path.basename(projectDir) || 'docs'
59
59
  const contentDir = path.join(projectDir, options.content)
60
60
 
61
- // Create content directory if it doesn't exist
61
+ // Create content directory
62
62
  if (!fs.existsSync(contentDir)) {
63
63
  fs.mkdirSync(contentDir, { recursive: true })
64
- console.log(chalk.green(''), 'Created', contentDir)
64
+ console.log(chalk.green('\u2713'), 'Created', contentDir)
65
65
  }
66
66
 
67
- // Create or update package.json in project root
67
+ // Create or update package.json
68
68
  const packageJsonPath = path.join(projectDir, 'package.json')
69
69
  if (!fs.existsSync(packageJsonPath)) {
70
70
  fs.writeFileSync(packageJsonPath, JSON.stringify(createPackageJson(dirName), null, 2) + '\n')
71
- console.log(chalk.green(''), 'Created', packageJsonPath)
71
+ console.log(chalk.green('\u2713'), 'Created', packageJsonPath)
72
72
  } else {
73
73
  const existing = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
74
74
  const template = createPackageJson(dirName)
75
75
  let updated = false
76
76
 
77
- // Merge missing scripts
77
+ if (existing.type !== 'module') {
78
+ existing.type = 'module'
79
+ updated = true
80
+ }
81
+
78
82
  if (!existing.scripts) existing.scripts = {}
79
83
  for (const [key, value] of Object.entries(template.scripts as Record<string, string>)) {
80
84
  if (!existing.scripts[key]) {
@@ -83,7 +87,6 @@ export const initCommand = new Command('init')
83
87
  }
84
88
  }
85
89
 
86
- // Merge missing dependencies
87
90
  if (!existing.dependencies) existing.dependencies = {}
88
91
  for (const [key, value] of Object.entries(template.dependencies as Record<string, string>)) {
89
92
  if (!existing.dependencies[key]) {
@@ -92,7 +95,6 @@ export const initCommand = new Command('init')
92
95
  }
93
96
  }
94
97
 
95
- // Merge missing devDependencies
96
98
  if (!existing.devDependencies) existing.devDependencies = {}
97
99
  for (const [key, value] of Object.entries(template.devDependencies as Record<string, string>)) {
98
100
  if (!existing.devDependencies[key]) {
@@ -103,41 +105,42 @@ export const initCommand = new Command('init')
103
105
 
104
106
  if (updated) {
105
107
  fs.writeFileSync(packageJsonPath, JSON.stringify(existing, null, 2) + '\n')
106
- console.log(chalk.green(''), 'Updated', packageJsonPath, 'with missing scripts/deps')
108
+ console.log(chalk.green('\u2713'), 'Updated', packageJsonPath)
107
109
  } else {
108
- console.log(chalk.yellow(''), packageJsonPath, 'already has all required entries')
110
+ console.log(chalk.yellow('\u26a0'), packageJsonPath, 'already has all required entries')
109
111
  }
110
112
  }
111
113
 
112
- // Create chronicle.yaml in project root
114
+ // Create chronicle.yaml
113
115
  const configPath = path.join(projectDir, 'chronicle.yaml')
114
116
  if (!fs.existsSync(configPath)) {
115
117
  fs.writeFileSync(configPath, stringify(createConfig()))
116
- console.log(chalk.green(''), 'Created', configPath)
118
+ console.log(chalk.green('\u2713'), 'Created', configPath)
117
119
  } else {
118
- console.log(chalk.yellow(''), configPath, 'already exists')
120
+ console.log(chalk.yellow('\u26a0'), configPath, 'already exists')
119
121
  }
120
122
 
121
- // Create sample index.mdx only if content dir is empty
123
+ // Create sample index.mdx
122
124
  const contentFiles = fs.readdirSync(contentDir)
123
125
  if (contentFiles.length === 0) {
124
126
  const indexPath = path.join(contentDir, 'index.mdx')
125
127
  fs.writeFileSync(indexPath, sampleMdx)
126
- console.log(chalk.green(''), 'Created', indexPath)
128
+ console.log(chalk.green('\u2713'), 'Created', indexPath)
127
129
  }
128
130
 
129
- // Add .chronicle to .gitignore
131
+ // Update .gitignore
130
132
  const gitignorePath = path.join(projectDir, '.gitignore')
131
- const chronicleEntry = '.chronicle'
133
+ const gitignoreEntries = ['node_modules', 'dist']
132
134
  if (fs.existsSync(gitignorePath)) {
133
135
  const existing = fs.readFileSync(gitignorePath, 'utf-8')
134
- if (!existing.includes(chronicleEntry)) {
135
- fs.appendFileSync(gitignorePath, `\n${chronicleEntry}\n`)
136
- console.log(chalk.green(''), 'Added .chronicle to .gitignore')
136
+ const missing = gitignoreEntries.filter(e => !existing.includes(e))
137
+ if (missing.length > 0) {
138
+ fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`)
139
+ console.log(chalk.green('\u2713'), 'Added', missing.join(', '), 'to .gitignore')
137
140
  }
138
141
  } else {
139
- fs.writeFileSync(gitignorePath, `${chronicleEntry}\n`)
140
- console.log(chalk.green(''), 'Created .gitignore with .chronicle')
142
+ fs.writeFileSync(gitignorePath, `${gitignoreEntries.join('\n')}\n`)
143
+ console.log(chalk.green('\u2713'), 'Created .gitignore')
141
144
  }
142
145
 
143
146
  // Install dependencies
@@ -145,11 +148,7 @@ export const initCommand = new Command('init')
145
148
  console.log(chalk.cyan(`\nInstalling dependencies with ${pm}...`))
146
149
  execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' })
147
150
 
148
- // Scaffold .chronicle/ directory
149
- loadCLIConfig(contentDir)
150
- scaffoldDir(contentDir)
151
-
152
151
  const runCmd = pm === 'npm' ? 'npx' : pm === 'bun' ? 'bunx' : `${pm} dlx`
153
- console.log(chalk.green('\n Chronicle initialized!'))
152
+ console.log(chalk.green('\n\u2713 Chronicle initialized!'))
154
153
  console.log('\nRun', chalk.cyan(`${runCmd} chronicle dev`), 'to start development server')
155
154
  })
@@ -1,53 +1,55 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import fs from 'fs'
5
3
  import chalk from 'chalk'
6
- import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
7
6
 
8
7
  export const serveCommand = new Command('serve')
9
8
  .description('Build and start production server')
10
9
  .option('-p, --port <port>', 'Port number', '3000')
11
- .action((options) => {
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
- }
10
+ .option('-c, --content <path>', 'Content directory')
11
+ .option('-o, --outDir <path>', 'Output directory', 'dist')
12
+ .action(async (options) => {
13
+ const contentDir = resolveContentDir(options.content)
14
+ const port = parseInt(options.port, 10)
15
+ const outDir = path.resolve(options.outDir)
17
16
 
18
- const nextCli = resolveNextCli()
19
-
20
- const env = {
21
- ...process.env,
22
- CHRONICLE_PROJECT_ROOT: process.cwd(),
23
- CHRONICLE_CONTENT_DIR: './content',
24
- }
17
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
18
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
25
19
 
20
+ // Build
26
21
  console.log(chalk.cyan('Building for production...'))
27
22
 
28
- const buildChild = spawn(process.execPath, [nextCli, 'build'], {
29
- stdio: 'inherit',
30
- cwd: scaffoldPath,
31
- env,
32
- })
23
+ const { build } = await import('vite')
24
+ const { createViteConfig } = await import('@/server/vite-config')
33
25
 
34
- process.once('SIGINT', () => buildChild.kill('SIGINT'))
35
- process.once('SIGTERM', () => buildChild.kill('SIGTERM'))
26
+ const baseConfig = await createViteConfig({ root: PACKAGE_ROOT, contentDir })
36
27
 
37
- buildChild.on('close', (code) => {
38
- if (code !== 0) {
39
- console.log(chalk.red('Build failed'))
40
- process.exit(code ?? 1)
41
- }
28
+ await build({
29
+ ...baseConfig,
30
+ build: {
31
+ outDir: path.join(outDir, 'client'),
32
+ ssrManifest: true,
33
+ rolldownOptions: {
34
+ input: path.resolve(PACKAGE_ROOT, 'src/server/index.html'),
35
+ },
36
+ },
37
+ })
42
38
 
43
- console.log(chalk.cyan('Starting production server...'))
39
+ await build({
40
+ ...baseConfig,
41
+ ssr: {
42
+ noExternal: true,
43
+ },
44
+ build: {
45
+ outDir: path.join(outDir, 'server'),
46
+ ssr: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts'),
47
+ },
48
+ })
44
49
 
45
- const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
46
- stdio: 'inherit',
47
- cwd: scaffoldPath,
48
- env,
49
- })
50
+ // Start
51
+ console.log(chalk.cyan('Starting production server...'))
50
52
 
51
- attachLifecycleHandlers(startChild)
52
- })
53
+ const { startProdServer } = await import('@/server/prod')
54
+ await startProdServer({ port, root: PACKAGE_ROOT, distDir: outDir })
53
55
  })
@@ -1,33 +1,24 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import fs from 'fs'
5
3
  import chalk from 'chalk'
6
- import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils'
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
7
6
 
8
7
  export const startCommand = new Command('start')
9
8
  .description('Start production server')
10
9
  .option('-p, --port <port>', 'Port number', '3000')
11
- .action((options) => {
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
- }
10
+ .option('-c, --content <path>', 'Content directory')
11
+ .option('-d, --dist <path>', 'Dist directory', 'dist')
12
+ .action(async (options) => {
13
+ const contentDir = resolveContentDir(options.content)
14
+ const port = parseInt(options.port, 10)
15
+ const distDir = path.resolve(options.dist)
17
16
 
18
- const nextCli = resolveNextCli()
17
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
18
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
19
19
 
20
20
  console.log(chalk.cyan('Starting production server...'))
21
21
 
22
- const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
23
- stdio: 'inherit',
24
- cwd: scaffoldPath,
25
- env: {
26
- ...process.env,
27
- CHRONICLE_PROJECT_ROOT: process.cwd(),
28
- CHRONICLE_CONTENT_DIR: './content',
29
- },
30
- })
31
-
32
- attachLifecycleHandlers(child)
22
+ const { startProdServer } = await import('@/server/prod')
23
+ await startProdServer({ port, root: PACKAGE_ROOT, distDir })
33
24
  })
@@ -28,7 +28,7 @@ export function loadCLIConfig(contentDir: string): CLIConfig {
28
28
  const configPath = resolveConfigPath(contentDir)
29
29
 
30
30
  if (!configPath) {
31
- console.log(chalk.red('Error: chronicle.yaml not found in'), process.cwd(), 'or', contentDir)
31
+ console.log(chalk.red(`Error: chronicle.yaml not found in '${process.cwd()}' or '${contentDir}'`))
32
32
  console.log(chalk.gray(`Run 'chronicle init' to create one`))
33
33
  process.exit(1)
34
34
  }
@@ -1,3 +1,2 @@
1
1
  export * from './config'
2
- export * from './process'
3
- export * from './scaffold'
2
+ export * from './resolve'
@@ -1,4 +1,6 @@
1
1
  import path from 'path'
2
2
  import { fileURLToPath } from 'url'
3
3
 
4
+ // After bundling: dist/cli/index.js → ../.. = package root
5
+ // After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
4
6
  export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
@@ -1,33 +1,7 @@
1
- import { execSync } from 'child_process'
2
- import { createRequire } from 'module'
3
1
  import fs from 'fs'
4
2
  import path from 'path'
5
- import chalk from 'chalk'
6
3
  import { PACKAGE_ROOT } from './resolve'
7
4
 
8
- const COPY_FILES = ['src', 'source.config.ts', 'tsconfig.json']
9
-
10
- function copyRecursive(src: string, dest: string) {
11
- const stat = fs.statSync(src)
12
- if (stat.isDirectory()) {
13
- fs.mkdirSync(dest, { recursive: true })
14
- for (const entry of fs.readdirSync(src)) {
15
- copyRecursive(path.join(src, entry), path.join(dest, entry))
16
- }
17
- } else {
18
- fs.copyFileSync(src, dest)
19
- }
20
- }
21
-
22
- function ensureRemoved(targetPath: string) {
23
- try {
24
- fs.lstatSync(targetPath)
25
- fs.rmSync(targetPath, { recursive: true, force: true })
26
- } catch {
27
- // nothing exists, proceed
28
- }
29
- }
30
-
31
5
  export function detectPackageManager(): string {
32
6
  if (process.env.npm_config_user_agent) {
33
7
  return process.env.npm_config_user_agent.split('/')[0]
@@ -39,93 +13,8 @@ export function detectPackageManager(): string {
39
13
  return 'npm'
40
14
  }
41
15
 
42
- function generateNextConfig(scaffoldPath: string) {
43
- const config = `import { createMDX } from 'fumadocs-mdx/next'
44
-
45
- const withMDX = createMDX()
46
-
47
- /** @type {import('next').NextConfig} */
48
- const nextConfig = {
49
- reactStrictMode: true,
50
- }
51
-
52
- export default withMDX(nextConfig)
53
- `
54
- fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config)
55
- }
56
-
57
- function createPackageJson(): Record<string, unknown> {
58
- return {
59
- name: 'chronicle-docs',
60
- private: true,
61
- dependencies: {
62
- '@raystack/chronicle': 'latest',
63
- },
64
- devDependencies: {
65
- '@raystack/tools-config': '0.56.0',
66
- 'openapi-types': '^12.1.3',
67
- typescript: '5.9.3',
68
- '@types/react': '^19.2.10',
69
- '@types/node': '^25.1.0',
70
- },
71
- }
72
- }
73
-
74
- function ensureDeps() {
75
- const cwd = process.cwd()
76
- const cwdPkgJson = path.join(cwd, 'package.json')
77
- const cwdNodeModules = path.join(cwd, 'node_modules')
78
-
79
- if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) {
80
- // Case 1: existing project with deps installed
81
- return
82
- }
83
-
84
- // Case 2: no package.json — create in cwd and install
85
- if (!fs.existsSync(cwdPkgJson)) {
86
- fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n')
87
- }
88
-
89
- if (!fs.existsSync(cwdNodeModules)) {
90
- const pm = detectPackageManager()
91
- console.log(chalk.cyan(`Installing dependencies with ${pm}...`))
92
- execSync(`${pm} install`, { cwd, stdio: 'inherit' })
93
- }
94
- }
95
-
96
- export function resolveNextCli(): string {
97
- const cwdRequire = createRequire(path.join(process.cwd(), 'package.json'))
98
- return cwdRequire.resolve('next/dist/bin/next')
99
- }
100
-
101
- export function scaffoldDir(contentDir: string): string {
102
- const scaffoldPath = path.join(process.cwd(), '.chronicle')
103
-
104
- // Create .chronicle/ if not exists
105
- if (!fs.existsSync(scaffoldPath)) {
106
- fs.mkdirSync(scaffoldPath, { recursive: true })
107
- }
108
-
109
- // Copy package files
110
- for (const name of COPY_FILES) {
111
- const src = path.join(PACKAGE_ROOT, name)
112
- const dest = path.join(scaffoldPath, name)
113
- ensureRemoved(dest)
114
- copyRecursive(src, dest)
115
- }
116
-
117
- // Generate next.config.mjs
118
- generateNextConfig(scaffoldPath)
119
-
120
- // Symlink content dir
121
- const contentLink = path.join(scaffoldPath, 'content')
122
- ensureRemoved(contentLink)
123
- fs.symlinkSync(path.resolve(contentDir), contentLink)
124
-
125
- // Ensure dependencies are available
126
- ensureDeps()
127
-
128
- console.log(chalk.gray(`Scaffold: ${scaffoldPath}`))
129
-
130
- return scaffoldPath
16
+ export function getChronicleVersion(): string {
17
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
18
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
19
+ return pkg.version
131
20
  }