From 45e3f5517c050f753646b56cd321054a9aa7c3cc Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 13:29:18 +0530 Subject: [PATCH 01/16] fix: resolve next binary via createRequire instead of hardcoded .bin path When installed via npx/bunx or as a dependency, next gets hoisted and node_modules/.bin/next doesn't exist inside the chronicle package dir. Use createRequire + require.resolve to find next/dist/bin/next via Node's module resolution which handles hoisting automatically. Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/bin/chronicle.js | 0 packages/chronicle/src/cli/commands/build.ts | 6 ++++-- packages/chronicle/src/cli/commands/dev.ts | 6 ++++-- packages/chronicle/src/cli/commands/serve.ts | 8 +++++--- packages/chronicle/src/cli/commands/start.ts | 6 ++++-- 5 files changed, 17 insertions(+), 9 deletions(-) mode change 100644 => 100755 packages/chronicle/bin/chronicle.js diff --git a/packages/chronicle/bin/chronicle.js b/packages/chronicle/bin/chronicle.js old mode 100644 new mode 100755 diff --git a/packages/chronicle/src/cli/commands/build.ts b/packages/chronicle/src/cli/commands/build.ts index ef11356..4bd4332 100644 --- a/packages/chronicle/src/cli/commands/build.ts +++ b/packages/chronicle/src/cli/commands/build.ts @@ -2,11 +2,13 @@ import { Command } from 'commander' import { spawn } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' +import { createRequire } from 'module' import chalk from 'chalk' import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +const require = createRequire(import.meta.url) const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') -const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next') +const nextCli = require.resolve('next/dist/bin/next') export const buildCommand = new Command('build') .description('Build for production') @@ -18,7 +20,7 @@ export const buildCommand = new Command('build') console.log(chalk.cyan('Building for production...')) console.log(chalk.gray(`Content: ${contentDir}`)) - const child = spawn(nextBin, ['build'], { + const child = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', cwd: PACKAGE_ROOT, env: { diff --git a/packages/chronicle/src/cli/commands/dev.ts b/packages/chronicle/src/cli/commands/dev.ts index 513d2d9..3606447 100644 --- a/packages/chronicle/src/cli/commands/dev.ts +++ b/packages/chronicle/src/cli/commands/dev.ts @@ -2,11 +2,13 @@ import { Command } from 'commander' import { spawn } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' +import { createRequire } from 'module' import chalk from 'chalk' import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +const require = createRequire(import.meta.url) const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') -const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next') +const nextCli = require.resolve('next/dist/bin/next') export const devCommand = new Command('dev') .description('Start development server') @@ -19,7 +21,7 @@ export const devCommand = new Command('dev') console.log(chalk.cyan('Starting dev server...')) console.log(chalk.gray(`Content: ${contentDir}`)) - const child = spawn(nextBin, ['dev', '-p', options.port], { + const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], { stdio: 'inherit', cwd: PACKAGE_ROOT, env: { diff --git a/packages/chronicle/src/cli/commands/serve.ts b/packages/chronicle/src/cli/commands/serve.ts index 6d9cb53..9435f5b 100644 --- a/packages/chronicle/src/cli/commands/serve.ts +++ b/packages/chronicle/src/cli/commands/serve.ts @@ -2,11 +2,13 @@ import { Command } from 'commander' import { spawn } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' +import { createRequire } from 'module' import chalk from 'chalk' import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +const require = createRequire(import.meta.url) const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') -const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next') +const nextCli = require.resolve('next/dist/bin/next') export const serveCommand = new Command('serve') .description('Build and start production server') @@ -24,7 +26,7 @@ export const serveCommand = new Command('serve') console.log(chalk.cyan('Building for production...')) console.log(chalk.gray(`Content: ${contentDir}`)) - const buildChild = spawn(nextBin, ['build'], { + const buildChild = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', cwd: PACKAGE_ROOT, env, @@ -41,7 +43,7 @@ export const serveCommand = new Command('serve') console.log(chalk.cyan('Starting production server...')) - const startChild = spawn(nextBin, ['start', '-p', options.port], { + const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], { stdio: 'inherit', cwd: PACKAGE_ROOT, env, diff --git a/packages/chronicle/src/cli/commands/start.ts b/packages/chronicle/src/cli/commands/start.ts index f1081c6..e4dfc60 100644 --- a/packages/chronicle/src/cli/commands/start.ts +++ b/packages/chronicle/src/cli/commands/start.ts @@ -2,11 +2,13 @@ import { Command } from 'commander' import { spawn } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' +import { createRequire } from 'module' import chalk from 'chalk' import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +const require = createRequire(import.meta.url) const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') -const nextBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'next.cmd' : 'next') +const nextCli = require.resolve('next/dist/bin/next') export const startCommand = new Command('start') .description('Start production server') @@ -19,7 +21,7 @@ export const startCommand = new Command('start') console.log(chalk.cyan('Starting production server...')) console.log(chalk.gray(`Content: ${contentDir}`)) - const child = spawn(nextBin, ['start', '-p', options.port], { + const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], { stdio: 'inherit', cwd: PACKAGE_ROOT, env: { From c47312fdb706757ba303b7578625ec474b4bcc0b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 13:34:05 +0530 Subject: [PATCH 02/16] fix: resolve chronicle.yaml from cwd first, then content dir Allows placing chronicle.yaml in project root when using chronicle as a library dependency, while still supporting it inside content dir. Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/cli/utils/config.ts | 14 +++++++++++--- packages/chronicle/src/lib/config.ts | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/chronicle/src/cli/utils/config.ts b/packages/chronicle/src/cli/utils/config.ts index 76c90bf..49092e6 100644 --- a/packages/chronicle/src/cli/utils/config.ts +++ b/packages/chronicle/src/cli/utils/config.ts @@ -16,11 +16,19 @@ export function resolveContentDir(contentFlag?: string): string { return process.cwd() } +function resolveConfigPath(contentDir: string): string | null { + const cwdPath = path.join(process.cwd(), 'chronicle.yaml') + if (fs.existsSync(cwdPath)) return cwdPath + const contentPath = path.join(contentDir, 'chronicle.yaml') + if (fs.existsSync(contentPath)) return contentPath + return null +} + export function loadCLIConfig(contentDir: string): CLIConfig { - const configPath = path.join(contentDir, 'chronicle.yaml') + const configPath = resolveConfigPath(contentDir) - if (!fs.existsSync(configPath)) { - console.log(chalk.red('Error: chronicle.yaml not found in'), contentDir) + if (!configPath) { + console.log(chalk.red('Error: chronicle.yaml not found in'), process.cwd(), 'or', contentDir) console.log(chalk.gray(`Run 'chronicle init' to create one`)) process.exit(1) } diff --git a/packages/chronicle/src/lib/config.ts b/packages/chronicle/src/lib/config.ts index e647d2b..74144dc 100644 --- a/packages/chronicle/src/lib/config.ts +++ b/packages/chronicle/src/lib/config.ts @@ -11,11 +11,21 @@ const defaultConfig: ChronicleConfig = { search: { enabled: true, placeholder: 'Search...' }, } +function resolveConfigPath(): string | null { + const cwdPath = path.join(process.cwd(), CONFIG_FILE) + if (fs.existsSync(cwdPath)) return cwdPath + const contentDir = process.env.CHRONICLE_CONTENT_DIR + if (contentDir) { + const contentPath = path.join(contentDir, CONFIG_FILE) + if (fs.existsSync(contentPath)) return contentPath + } + return null +} + export function loadConfig(): ChronicleConfig { - const dir = process.env.CHRONICLE_CONTENT_DIR ?? process.cwd() - const configPath = path.join(dir, CONFIG_FILE) + const configPath = resolveConfigPath() - if (!fs.existsSync(configPath)) { + if (!configPath) { return defaultConfig } From e11f92422de5dc433e1ea3c16dc91ed541178fb4 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 13:46:14 +0530 Subject: [PATCH 03/16] ci: add canary release workflow for PR builds Publishes canary npm release on every PR commit with version format 0.1.0-canary. using --tag canary. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/canary.yml diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 0000000..22caf16 --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,40 @@ +name: canary + +on: + pull_request: + types: [opened, synchronize] + +jobs: + canary-release: + name: Publish canary to npm + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: ./packages/chronicle + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + working-directory: . + + - name: Build CLI + run: bun build-cli.ts + + - name: Set canary version + run: | + SHORT_SHA=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7) + VERSION=$(jq -r .version package.json)-canary.${SHORT_SHA} + jq --arg v "$VERSION" '.version = $v' package.json > package.tmp.json + mv package.tmp.json package.json + echo "Published version: $VERSION" + + - name: Publish + run: bun publish --tag canary --access public + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} From 0c91312d15a5867003e593c99a8e3c3a831b2529 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 20:15:15 +0530 Subject: [PATCH 04/16] feat: scaffold .chronicle/ directory instead of running Next.js from node_modules Turbopack refuses to compile .ts/.tsx from node_modules. Instead of running Next.js with cwd: PACKAGE_ROOT (inside node_modules), the CLI now scaffolds a .chronicle/ directory in the user's project with copied source files and a content symlink. Next.js runs from .chronicle/ where Turbopack compiles normally. - Add scaffold utility that copies src/, source.config.ts, tsconfig.json and generates next.config.mjs - Update dev/build/start/serve commands to use scaffold cwd - Update init command to create package.json with deps, content/ subdirectory, and auto-install via detected package manager - Default content dir changed from cwd to ./content - Add .chronicle to .gitignore in init Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/cli/commands/build.ts | 10 +- packages/chronicle/src/cli/commands/dev.ts | 10 +- packages/chronicle/src/cli/commands/init.ts | 73 +++++++++++- packages/chronicle/src/cli/commands/serve.ts | 12 +- packages/chronicle/src/cli/commands/start.ts | 10 +- packages/chronicle/src/cli/utils/config.ts | 2 +- packages/chronicle/src/cli/utils/index.ts | 1 + packages/chronicle/src/cli/utils/scaffold.ts | 118 +++++++++++++++++++ 8 files changed, 204 insertions(+), 32 deletions(-) create mode 100644 packages/chronicle/src/cli/utils/scaffold.ts diff --git a/packages/chronicle/src/cli/commands/build.ts b/packages/chronicle/src/cli/commands/build.ts index 4bd4332..37db4c8 100644 --- a/packages/chronicle/src/cli/commands/build.ts +++ b/packages/chronicle/src/cli/commands/build.ts @@ -1,13 +1,10 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import path from 'path' -import { fileURLToPath } from 'url' import { createRequire } from 'module' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' const require = createRequire(import.meta.url) -const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') const nextCli = require.resolve('next/dist/bin/next') export const buildCommand = new Command('build') @@ -16,16 +13,17 @@ export const buildCommand = new Command('build') .action((options) => { const contentDir = resolveContentDir(options.content) loadCLIConfig(contentDir) + const scaffoldPath = scaffoldDir(contentDir) console.log(chalk.cyan('Building for production...')) console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', - cwd: PACKAGE_ROOT, + cwd: scaffoldPath, env: { ...process.env, - CHRONICLE_CONTENT_DIR: contentDir, + CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/cli/commands/dev.ts b/packages/chronicle/src/cli/commands/dev.ts index 3606447..6026454 100644 --- a/packages/chronicle/src/cli/commands/dev.ts +++ b/packages/chronicle/src/cli/commands/dev.ts @@ -1,13 +1,10 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import path from 'path' -import { fileURLToPath } from 'url' import { createRequire } from 'module' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' const require = createRequire(import.meta.url) -const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') const nextCli = require.resolve('next/dist/bin/next') export const devCommand = new Command('dev') @@ -17,16 +14,17 @@ export const devCommand = new Command('dev') .action((options) => { const contentDir = resolveContentDir(options.content) loadCLIConfig(contentDir) + const scaffoldPath = scaffoldDir(contentDir) console.log(chalk.cyan('Starting dev server...')) console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], { stdio: 'inherit', - cwd: PACKAGE_ROOT, + cwd: scaffoldPath, env: { ...process.env, - CHRONICLE_CONTENT_DIR: contentDir, + CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/cli/commands/init.ts b/packages/chronicle/src/cli/commands/init.ts index 212a75f..d1d1704 100644 --- a/packages/chronicle/src/cli/commands/init.ts +++ b/packages/chronicle/src/cli/commands/init.ts @@ -1,10 +1,12 @@ import { Command } from 'commander' +import { execSync } from 'child_process' import fs from 'fs' import path from 'path' import chalk from 'chalk' import { stringify } from 'yaml' import type { ChronicleConfig } from '@/types' + function createConfig(): ChronicleConfig { return { title: 'My Documentation', @@ -14,6 +16,28 @@ function createConfig(): ChronicleConfig { } } +function createPackageJson(name: string): Record { + return { + name, + private: true, + scripts: { + dev: 'chronicle dev', + build: 'chronicle build', + start: 'chronicle start', + }, + dependencies: { + '@raystack/chronicle': 'latest', + }, + devDependencies: { + '@raystack/tools-config': '0.56.0', + 'openapi-types': '^12.1.3', + typescript: '5.9.3', + '@types/react': '^19.2.10', + '@types/node': '^25.1.0', + }, + } +} + const sampleMdx = `--- title: Welcome description: Getting started with your documentation @@ -27,9 +51,17 @@ This is your documentation home page. export const initCommand = new Command('init') .description('Initialize a new Chronicle project') - .option('-d, --dir ', 'Content directory', '.') + .option('-d, --dir ', 'Project directory', '.') .action((options) => { - const contentDir = path.resolve(options.dir) + const projectDir = path.resolve(options.dir) + const dirName = path.basename(projectDir) || 'docs' + const contentDir = path.join(projectDir, 'content') + + // Create project directory + if (!fs.existsSync(projectDir)) { + fs.mkdirSync(projectDir, { recursive: true }) + console.log(chalk.green('✓'), 'Created', projectDir) + } // Create content directory if (!fs.existsSync(contentDir)) { @@ -37,8 +69,17 @@ export const initCommand = new Command('init') console.log(chalk.green('✓'), 'Created', contentDir) } - // Create chronicle.yaml - const configPath = path.join(contentDir, 'chronicle.yaml') + // Create package.json in project root + const packageJsonPath = path.join(projectDir, 'package.json') + if (!fs.existsSync(packageJsonPath)) { + fs.writeFileSync(packageJsonPath, JSON.stringify(createPackageJson(dirName), null, 2) + '\n') + console.log(chalk.green('✓'), 'Created', packageJsonPath) + } else { + console.log(chalk.yellow('⚠'), packageJsonPath, 'already exists') + } + + // Create chronicle.yaml in project root + const configPath = path.join(projectDir, 'chronicle.yaml') if (!fs.existsSync(configPath)) { fs.writeFileSync(configPath, stringify(createConfig())) console.log(chalk.green('✓'), 'Created', configPath) @@ -46,13 +87,33 @@ export const initCommand = new Command('init') console.log(chalk.yellow('⚠'), configPath, 'already exists') } - // Create sample index.mdx + // Create sample index.mdx in content/ const indexPath = path.join(contentDir, 'index.mdx') if (!fs.existsSync(indexPath)) { fs.writeFileSync(indexPath, sampleMdx) console.log(chalk.green('✓'), 'Created', indexPath) } + // Add .chronicle to .gitignore + const gitignorePath = path.join(projectDir, '.gitignore') + const chronicleEntry = '.chronicle' + if (fs.existsSync(gitignorePath)) { + const existing = fs.readFileSync(gitignorePath, 'utf-8') + if (!existing.includes(chronicleEntry)) { + fs.appendFileSync(gitignorePath, `\n${chronicleEntry}\n`) + console.log(chalk.green('✓'), 'Added .chronicle to .gitignore') + } + } else { + fs.writeFileSync(gitignorePath, `${chronicleEntry}\n`) + console.log(chalk.green('✓'), 'Created .gitignore with .chronicle') + } + + // Install dependencies + const pm = (process.env.npm_config_user_agent || 'npm').split('/')[0] + console.log(chalk.cyan(`\nInstalling dependencies with ${pm}...`)) + execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' }) + + const runCmd = pm === 'npm' ? 'npx' : pm === 'bun' ? 'bunx' : `${pm} dlx` console.log(chalk.green('\n✓ Chronicle initialized!')) - console.log('\nRun', chalk.cyan('chronicle dev'), 'to start development server') + console.log('\nRun', chalk.cyan(`${runCmd} chronicle dev`), 'to start development server') }) diff --git a/packages/chronicle/src/cli/commands/serve.ts b/packages/chronicle/src/cli/commands/serve.ts index 9435f5b..07da181 100644 --- a/packages/chronicle/src/cli/commands/serve.ts +++ b/packages/chronicle/src/cli/commands/serve.ts @@ -1,13 +1,10 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import path from 'path' -import { fileURLToPath } from 'url' import { createRequire } from 'module' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' const require = createRequire(import.meta.url) -const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') const nextCli = require.resolve('next/dist/bin/next') export const serveCommand = new Command('serve') @@ -17,10 +14,11 @@ export const serveCommand = new Command('serve') .action((options) => { const contentDir = resolveContentDir(options.content) loadCLIConfig(contentDir) + const scaffoldPath = scaffoldDir(contentDir) const env = { ...process.env, - CHRONICLE_CONTENT_DIR: contentDir, + CHRONICLE_CONTENT_DIR: './content', } console.log(chalk.cyan('Building for production...')) @@ -28,7 +26,7 @@ export const serveCommand = new Command('serve') const buildChild = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', - cwd: PACKAGE_ROOT, + cwd: scaffoldPath, env, }) @@ -45,7 +43,7 @@ export const serveCommand = new Command('serve') const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], { stdio: 'inherit', - cwd: PACKAGE_ROOT, + cwd: scaffoldPath, env, }) diff --git a/packages/chronicle/src/cli/commands/start.ts b/packages/chronicle/src/cli/commands/start.ts index e4dfc60..b951a6a 100644 --- a/packages/chronicle/src/cli/commands/start.ts +++ b/packages/chronicle/src/cli/commands/start.ts @@ -1,13 +1,10 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import path from 'path' -import { fileURLToPath } from 'url' import { createRequire } from 'module' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils' +import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' const require = createRequire(import.meta.url) -const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') const nextCli = require.resolve('next/dist/bin/next') export const startCommand = new Command('start') @@ -17,16 +14,17 @@ export const startCommand = new Command('start') .action((options) => { const contentDir = resolveContentDir(options.content) loadCLIConfig(contentDir) + const scaffoldPath = scaffoldDir(contentDir) console.log(chalk.cyan('Starting production server...')) console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], { stdio: 'inherit', - cwd: PACKAGE_ROOT, + cwd: scaffoldPath, env: { ...process.env, - CHRONICLE_CONTENT_DIR: contentDir, + CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/cli/utils/config.ts b/packages/chronicle/src/cli/utils/config.ts index 49092e6..2b8c0ad 100644 --- a/packages/chronicle/src/cli/utils/config.ts +++ b/packages/chronicle/src/cli/utils/config.ts @@ -13,7 +13,7 @@ export interface CLIConfig { export function resolveContentDir(contentFlag?: string): string { if (contentFlag) return path.resolve(contentFlag) if (process.env.CHRONICLE_CONTENT_DIR) return path.resolve(process.env.CHRONICLE_CONTENT_DIR) - return process.cwd() + return path.resolve('content') } function resolveConfigPath(contentDir: string): string | null { diff --git a/packages/chronicle/src/cli/utils/index.ts b/packages/chronicle/src/cli/utils/index.ts index 645d5a9..7b2a393 100644 --- a/packages/chronicle/src/cli/utils/index.ts +++ b/packages/chronicle/src/cli/utils/index.ts @@ -1,2 +1,3 @@ export * from './config' export * from './process' +export * from './scaffold' diff --git a/packages/chronicle/src/cli/utils/scaffold.ts b/packages/chronicle/src/cli/utils/scaffold.ts new file mode 100644 index 0000000..52966ae --- /dev/null +++ b/packages/chronicle/src/cli/utils/scaffold.ts @@ -0,0 +1,118 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import path from 'path' +import chalk from 'chalk' +import { PACKAGE_ROOT } from './resolve' + +const COPY_FILES = ['src', 'source.config.ts', 'tsconfig.json'] + +function copyRecursive(src: string, dest: string) { + const stat = fs.statSync(src) + if (stat.isDirectory()) { + fs.mkdirSync(dest, { recursive: true }) + for (const entry of fs.readdirSync(src)) { + copyRecursive(path.join(src, entry), path.join(dest, entry)) + } + } else { + fs.copyFileSync(src, dest) + } +} + +function ensureRemoved(targetPath: string) { + try { + fs.lstatSync(targetPath) + fs.rmSync(targetPath, { recursive: true, force: true }) + } catch { + // nothing exists, proceed + } +} + +function detectPackageManager(): string { + return (process.env.npm_config_user_agent || 'npm').split('/')[0] +} + +function generateNextConfig(scaffoldPath: string) { + const config = `import { createMDX } from 'fumadocs-mdx/next' + +const withMDX = createMDX() + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +export default withMDX(nextConfig) +` + fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config) +} + +function createPackageJson(): Record { + return { + name: 'chronicle-docs', + private: true, + dependencies: { + '@raystack/chronicle': 'latest', + }, + devDependencies: { + '@raystack/tools-config': '0.56.0', + 'openapi-types': '^12.1.3', + typescript: '5.9.3', + '@types/react': '^19.2.10', + '@types/node': '^25.1.0', + }, + } +} + +function ensureDeps() { + const cwd = process.cwd() + const cwdPkgJson = path.join(cwd, 'package.json') + const cwdNodeModules = path.join(cwd, 'node_modules') + + if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) { + // Case 1: existing project with deps installed + return + } + + // Case 2: no package.json — create in cwd and install + if (!fs.existsSync(cwdPkgJson)) { + fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n') + } + + if (!fs.existsSync(cwdNodeModules)) { + const pm = detectPackageManager() + console.log(chalk.cyan(`Installing dependencies with ${pm}...`)) + execSync(`${pm} install`, { cwd, stdio: 'inherit' }) + } +} + +export function scaffoldDir(contentDir: string): string { + const scaffoldPath = path.join(process.cwd(), '.chronicle') + + // Create .chronicle/ if not exists + if (!fs.existsSync(scaffoldPath)) { + fs.mkdirSync(scaffoldPath, { recursive: true }) + } + + // Copy package files + for (const name of COPY_FILES) { + const src = path.join(PACKAGE_ROOT, name) + const dest = path.join(scaffoldPath, name) + ensureRemoved(dest) + copyRecursive(src, dest) + } + + // Generate next.config.mjs + generateNextConfig(scaffoldPath) + + // Symlink content dir + const contentLink = path.join(scaffoldPath, 'content') + ensureRemoved(contentLink) + fs.symlinkSync(path.resolve(contentDir), contentLink) + + // Ensure dependencies are available + ensureDeps() + + console.log(chalk.gray(`Scaffold: ${scaffoldPath}`)) + + return scaffoldPath +} From 32741605b6f9dae84b68e09ba3683eb2543787ed Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 23:42:27 +0530 Subject: [PATCH 05/16] fix: move scaffolding to init, resolve next from cwd, detect package manager via lockfiles - Move scaffoldDir call from dev/build/start/serve into init command - Commands now require .chronicle/ to exist (fail with helpful message) - Remove --content and -d flags from commands and init - Add resolveNextCli() using createRequire to avoid duplicate Next.js instances - Detect package manager via npm_config_user_agent then lockfile fallback - Update Dockerfile to use bun add + chronicle init flow - Add resolve.ts for PACKAGE_ROOT derivation Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 21 ++++++++++--------- packages/chronicle/src/cli/commands/build.ts | 22 ++++++++++---------- packages/chronicle/src/cli/commands/dev.ts | 20 +++++++++--------- packages/chronicle/src/cli/commands/init.ts | 12 +++++++---- packages/chronicle/src/cli/commands/serve.ts | 20 +++++++++--------- packages/chronicle/src/cli/commands/start.ts | 20 +++++++++--------- packages/chronicle/src/cli/utils/resolve.ts | 4 ++++ packages/chronicle/src/cli/utils/scaffold.ts | 17 +++++++++++++-- 8 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 packages/chronicle/src/cli/utils/resolve.ts diff --git a/Dockerfile b/Dockerfile index aa2d507..701b8a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,22 +13,23 @@ WORKDIR /app/packages/chronicle COPY --from=deps /app /app COPY packages/chronicle ./ RUN bun build-cli.ts +RUN chmod +x bin/chronicle.js +RUN ln -s /app/packages/chronicle/bin/chronicle.js /usr/local/bin/chronicle + +# --- init project --- +WORKDIR /docs +RUN bun add /app/packages/chronicle +RUN chronicle init # --- runner --- FROM base AS runner -WORKDIR /app/packages/chronicle - -COPY --from=builder /app /app +WORKDIR /docs -RUN chmod +x bin/chronicle.js +COPY --from=builder /docs /docs +COPY --from=builder /app/packages/chronicle /app/packages/chronicle RUN ln -s /app/packages/chronicle/bin/chronicle.js /usr/local/bin/chronicle -RUN mkdir -p /app/content && ln -s /app/content /app/packages/chronicle/content - -VOLUME /app/content - -ENV CHRONICLE_CONTENT_DIR=./content -WORKDIR /app/packages/chronicle +VOLUME /docs/content EXPOSE 3000 diff --git a/packages/chronicle/src/cli/commands/build.ts b/packages/chronicle/src/cli/commands/build.ts index 37db4c8..f352a8c 100644 --- a/packages/chronicle/src/cli/commands/build.ts +++ b/packages/chronicle/src/cli/commands/build.ts @@ -1,22 +1,22 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import { createRequire } from 'module' +import path from 'path' +import fs from 'fs' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' - -const require = createRequire(import.meta.url) -const nextCli = require.resolve('next/dist/bin/next') +import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils' export const buildCommand = new Command('build') .description('Build for production') - .option('-c, --content ', 'Content directory') - .action((options) => { - const contentDir = resolveContentDir(options.content) - loadCLIConfig(contentDir) - const scaffoldPath = scaffoldDir(contentDir) + .action(() => { + const scaffoldPath = path.join(process.cwd(), '.chronicle') + if (!fs.existsSync(scaffoldPath)) { + console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.')) + process.exit(1) + } + + const nextCli = resolveNextCli() console.log(chalk.cyan('Building for production...')) - console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', diff --git a/packages/chronicle/src/cli/commands/dev.ts b/packages/chronicle/src/cli/commands/dev.ts index 6026454..bfcf092 100644 --- a/packages/chronicle/src/cli/commands/dev.ts +++ b/packages/chronicle/src/cli/commands/dev.ts @@ -1,23 +1,23 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import { createRequire } from 'module' +import path from 'path' +import fs from 'fs' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' - -const require = createRequire(import.meta.url) -const nextCli = require.resolve('next/dist/bin/next') +import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils' export const devCommand = new Command('dev') .description('Start development server') .option('-p, --port ', 'Port number', '3000') - .option('-c, --content ', 'Content directory') .action((options) => { - const contentDir = resolveContentDir(options.content) - loadCLIConfig(contentDir) - const scaffoldPath = scaffoldDir(contentDir) + const scaffoldPath = path.join(process.cwd(), '.chronicle') + if (!fs.existsSync(scaffoldPath)) { + console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.')) + process.exit(1) + } + + const nextCli = resolveNextCli() console.log(chalk.cyan('Starting dev server...')) - console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], { stdio: 'inherit', diff --git a/packages/chronicle/src/cli/commands/init.ts b/packages/chronicle/src/cli/commands/init.ts index d1d1704..a6f8422 100644 --- a/packages/chronicle/src/cli/commands/init.ts +++ b/packages/chronicle/src/cli/commands/init.ts @@ -5,6 +5,7 @@ import path from 'path' import chalk from 'chalk' import { stringify } from 'yaml' import type { ChronicleConfig } from '@/types' +import { loadCLIConfig, scaffoldDir, detectPackageManager } from '@/cli/utils' function createConfig(): ChronicleConfig { @@ -51,9 +52,8 @@ This is your documentation home page. export const initCommand = new Command('init') .description('Initialize a new Chronicle project') - .option('-d, --dir ', 'Project directory', '.') - .action((options) => { - const projectDir = path.resolve(options.dir) + .action(() => { + const projectDir = process.cwd() const dirName = path.basename(projectDir) || 'docs' const contentDir = path.join(projectDir, 'content') @@ -109,10 +109,14 @@ export const initCommand = new Command('init') } // Install dependencies - const pm = (process.env.npm_config_user_agent || 'npm').split('/')[0] + const pm = detectPackageManager() console.log(chalk.cyan(`\nInstalling dependencies with ${pm}...`)) execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' }) + // Scaffold .chronicle/ directory + loadCLIConfig(contentDir) + scaffoldDir(contentDir) + const runCmd = pm === 'npm' ? 'npx' : pm === 'bun' ? 'bunx' : `${pm} dlx` console.log(chalk.green('\n✓ Chronicle initialized!')) console.log('\nRun', chalk.cyan(`${runCmd} chronicle dev`), 'to start development server') diff --git a/packages/chronicle/src/cli/commands/serve.ts b/packages/chronicle/src/cli/commands/serve.ts index 07da181..20ea057 100644 --- a/packages/chronicle/src/cli/commands/serve.ts +++ b/packages/chronicle/src/cli/commands/serve.ts @@ -1,20 +1,21 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import { createRequire } from 'module' +import path from 'path' +import fs from 'fs' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' - -const require = createRequire(import.meta.url) -const nextCli = require.resolve('next/dist/bin/next') +import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils' export const serveCommand = new Command('serve') .description('Build and start production server') .option('-p, --port ', 'Port number', '3000') - .option('-c, --content ', 'Content directory') .action((options) => { - const contentDir = resolveContentDir(options.content) - loadCLIConfig(contentDir) - const scaffoldPath = scaffoldDir(contentDir) + const scaffoldPath = path.join(process.cwd(), '.chronicle') + if (!fs.existsSync(scaffoldPath)) { + console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.')) + process.exit(1) + } + + const nextCli = resolveNextCli() const env = { ...process.env, @@ -22,7 +23,6 @@ export const serveCommand = new Command('serve') } console.log(chalk.cyan('Building for production...')) - console.log(chalk.gray(`Content: ${contentDir}`)) const buildChild = spawn(process.execPath, [nextCli, 'build'], { stdio: 'inherit', diff --git a/packages/chronicle/src/cli/commands/start.ts b/packages/chronicle/src/cli/commands/start.ts index b951a6a..7975df1 100644 --- a/packages/chronicle/src/cli/commands/start.ts +++ b/packages/chronicle/src/cli/commands/start.ts @@ -1,23 +1,23 @@ import { Command } from 'commander' import { spawn } from 'child_process' -import { createRequire } from 'module' +import path from 'path' +import fs from 'fs' import chalk from 'chalk' -import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers, scaffoldDir } from '@/cli/utils' - -const require = createRequire(import.meta.url) -const nextCli = require.resolve('next/dist/bin/next') +import { attachLifecycleHandlers, resolveNextCli } from '@/cli/utils' export const startCommand = new Command('start') .description('Start production server') .option('-p, --port ', 'Port number', '3000') - .option('-c, --content ', 'Content directory') .action((options) => { - const contentDir = resolveContentDir(options.content) - loadCLIConfig(contentDir) - const scaffoldPath = scaffoldDir(contentDir) + const scaffoldPath = path.join(process.cwd(), '.chronicle') + if (!fs.existsSync(scaffoldPath)) { + console.log(chalk.red('Error: .chronicle/ not found. Run'), chalk.cyan('chronicle init'), chalk.red('first.')) + process.exit(1) + } + + const nextCli = resolveNextCli() console.log(chalk.cyan('Starting production server...')) - console.log(chalk.gray(`Content: ${contentDir}`)) const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], { stdio: 'inherit', diff --git a/packages/chronicle/src/cli/utils/resolve.ts b/packages/chronicle/src/cli/utils/resolve.ts new file mode 100644 index 0000000..3e4b5e1 --- /dev/null +++ b/packages/chronicle/src/cli/utils/resolve.ts @@ -0,0 +1,4 @@ +import path from 'path' +import { fileURLToPath } from 'url' + +export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') diff --git a/packages/chronicle/src/cli/utils/scaffold.ts b/packages/chronicle/src/cli/utils/scaffold.ts index 52966ae..5d7263d 100644 --- a/packages/chronicle/src/cli/utils/scaffold.ts +++ b/packages/chronicle/src/cli/utils/scaffold.ts @@ -1,4 +1,5 @@ import { execSync } from 'child_process' +import { createRequire } from 'module' import fs from 'fs' import path from 'path' import chalk from 'chalk' @@ -27,8 +28,15 @@ function ensureRemoved(targetPath: string) { } } -function detectPackageManager(): string { - return (process.env.npm_config_user_agent || 'npm').split('/')[0] +export function detectPackageManager(): string { + if (process.env.npm_config_user_agent) { + return process.env.npm_config_user_agent.split('/')[0] + } + const cwd = process.cwd() + if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun' + if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm' + if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn' + return 'npm' } function generateNextConfig(scaffoldPath: string) { @@ -85,6 +93,11 @@ function ensureDeps() { } } +export function resolveNextCli(): string { + const cwdRequire = createRequire(path.join(process.cwd(), 'package.json')) + return cwdRequire.resolve('next/dist/bin/next') +} + export function scaffoldDir(contentDir: string): string { const scaffoldPath = path.join(process.cwd(), '.chronicle') From 7c3201136da03d7883d6e88234371f77ac9eb784 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 23:56:05 +0530 Subject: [PATCH 06/16] fix: use .npmrc file for npm auth in canary workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 22caf16..74f3c57 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -32,9 +32,9 @@ jobs: VERSION=$(jq -r .version package.json)-canary.${SHORT_SHA} jq --arg v "$VERSION" '.version = $v' package.json > package.tmp.json mv package.tmp.json package.json - echo "Published version: $VERSION" + echo "Canary version: $VERSION" - name: Publish - run: bun publish --tag canary --access public - env: - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + bun publish --tag canary --access public From 995a12772c37a210c910c0118cf5abe979669f21 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 23:56:58 +0530 Subject: [PATCH 07/16] fix: write .npmrc to home dir for bun publish auth Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 74f3c57..3a12faa 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -36,5 +36,5 @@ jobs: - name: Publish run: | - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc bun publish --tag canary --access public From 3197f849cbae122200e476f27328253845e48ea5 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 12 Mar 2026 23:58:04 +0530 Subject: [PATCH 08/16] fix: revert canary publish to bun with NPM_CONFIG_TOKEN Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 3a12faa..2b0c869 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -35,6 +35,6 @@ jobs: echo "Canary version: $VERSION" - name: Publish - run: | - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - bun publish --tag canary --access public + run: bun publish --tag canary --access public + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} From ba8d278e4b99fb706a27e68eacb8bcdbb86d380a Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 09:27:51 +0530 Subject: [PATCH 09/16] debug: add npm whoami step to verify token Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 2b0c869..a0be34c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -34,6 +34,13 @@ jobs: mv package.tmp.json package.json echo "Canary version: $VERSION" + - name: Verify npm auth + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_CONFIG_TOKEN}" > ~/.npmrc + npm whoami + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish run: bun publish --tag canary --access public env: From 49063453f2a4e45ebd8aafe7fd2a29c572208340 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 10:13:52 +0530 Subject: [PATCH 10/16] fix: standalone tsconfig, openapi-types dep, content flag, update docs - Remove extends @raystack/tools-config from tsconfig.json (standalone) - Move openapi-types from devDependencies to dependencies - Add -c/--content flag to init for custom content dir name - Skip sample index.mdx if content dir already has files - Update CLI, Docker, and Getting Started docs for new scaffold flow Co-Authored-By: Claude Opus 4.6 --- docs/cli.mdx | 39 ++++++++++----------- docs/docker.mdx | 16 ++++----- docs/index.mdx | 32 +++++++++++------ packages/chronicle/package.json | 5 +-- packages/chronicle/src/cli/commands/init.ts | 20 +++++------ packages/chronicle/tsconfig.json | 16 ++++++++- 6 files changed, 74 insertions(+), 54 deletions(-) diff --git a/docs/cli.mdx b/docs/cli.mdx index 74b8d9b..84d87d4 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -10,7 +10,7 @@ Chronicle provides a CLI to initialize, develop, build, and serve your documenta ## init -Scaffold a new Chronicle project. +Initialize a new Chronicle project. Must be run before other commands. ```bash chronicle init [options] @@ -18,13 +18,20 @@ chronicle init [options] | Flag | Description | Default | |------|-------------|---------| -| `-d, --dir ` | Target directory | `.` (current directory) | +| `-c, --content ` | Content directory name | `content` | -Creates a `chronicle.yaml` config file and a sample `index.mdx` in the specified directory. +This creates: +- `chronicle.yaml` — site configuration +- `content/` (or custom name) — content directory with a sample `index.mdx` +- `package.json` — with `@raystack/chronicle` dependency (if not exists) +- `.chronicle/` — scaffolded build directory +- `.gitignore` — with `.chronicle` entry + +If the content directory already exists and has files, the sample `index.mdx` is skipped. ## dev -Start the development server with hot reload. +Start the development server with hot reload. Requires `chronicle init` first. ```bash chronicle dev [options] @@ -33,20 +40,15 @@ chronicle dev [options] | Flag | Description | Default | |------|-------------|---------| | `-p, --port ` | Port number | `3000` | -| `-c, --content ` | Content directory | Current directory | ## build -Build the site for production. +Build the site for production. Requires `chronicle init` first. ```bash -chronicle build [options] +chronicle build ``` -| Flag | Description | Default | -|------|-------------|---------| -| `-c, --content ` | Content directory | Current directory | - ## start Start the production server. Requires a prior `chronicle build`. @@ -58,11 +60,10 @@ chronicle start [options] | Flag | Description | Default | |------|-------------|---------| | `-p, --port ` | Port number | `3000` | -| `-c, --content ` | Content directory | Current directory | ## serve -Build and start the production server in one step. +Build and start the production server in one step. Requires `chronicle init` first. ```bash chronicle serve [options] @@ -71,14 +72,10 @@ chronicle serve [options] | Flag | Description | Default | |------|-------------|---------| | `-p, --port ` | Port number | `3000` | -| `-c, --content ` | Content directory | Current directory | - -## Content Directory Resolution -The content directory is resolved in this order: +## Config Resolution -1. `--content` CLI flag (highest priority) -2. `CHRONICLE_CONTENT_DIR` environment variable -3. Current working directory (default) +`chronicle.yaml` is resolved in this order: -The content directory must contain a `chronicle.yaml` file. +1. Current working directory +2. Content directory diff --git a/docs/docker.mdx b/docs/docker.mdx index 03d3def..4e0a252 100644 --- a/docs/docker.mdx +++ b/docs/docker.mdx @@ -19,23 +19,23 @@ docker pull raystack/chronicle The default command builds and starts a production server on port 3000. ```bash -docker run -p 3000:3000 -v ./content:/app/content raystack/chronicle +docker run -p 3000:3000 -v ./content:/docs/content raystack/chronicle ``` -Mount your content directory (containing `chronicle.yaml` and MDX files) to `/app/content`. +Mount your content directory (containing MDX files) to `/docs/content`. Place `chronicle.yaml` in the content directory or it will use defaults. ## Development Server Override the default command with `dev` for hot reload: ```bash -docker run -p 3000:3000 -v ./content:/app/content raystack/chronicle dev --port 3000 +docker run -p 3000:3000 -v ./content:/docs/content raystack/chronicle dev --port 3000 ``` ## Custom Port ```bash -docker run -p 8080:8080 -v ./content:/app/content raystack/chronicle serve --port 8080 +docker run -p 8080:8080 -v ./content:/docs/content raystack/chronicle serve --port 8080 ``` ## Docker Compose @@ -47,7 +47,7 @@ services: ports: - "3000:3000" volumes: - - ./content:/app/content + - ./content:/docs/content ``` ## Available Commands @@ -56,11 +56,11 @@ The entrypoint is `chronicle`, so any CLI command can be passed: ```bash # Build only -docker run -v ./content:/app/content raystack/chronicle build +docker run -v ./content:/docs/content raystack/chronicle build # Start pre-built server -docker run -p 3000:3000 -v ./content:/app/content raystack/chronicle start --port 3000 +docker run -p 3000:3000 -v ./content:/docs/content raystack/chronicle start --port 3000 # Build and start (default) -docker run -p 3000:3000 -v ./content:/app/content raystack/chronicle serve --port 3000 +docker run -p 3000:3000 -v ./content:/docs/content raystack/chronicle serve --port 3000 ``` diff --git a/docs/index.mdx b/docs/index.mdx index c02a328..5baaa03 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -24,7 +24,15 @@ chronicle init This creates: - `chronicle.yaml` — your site configuration -- `index.mdx` — a sample documentation page +- `content/` — content directory with a sample `index.mdx` +- `package.json` — with `@raystack/chronicle` dependency +- `.chronicle/` — scaffolded build directory (gitignored) + +To use an existing directory as content (e.g. `docs/`): + +```bash +chronicle init -c docs +``` ### 2. Start the development server @@ -39,13 +47,15 @@ Your docs site is now running at [http://localhost:3000](http://localhost:3000). Create `.mdx` files in your content directory. Each file becomes a page. Use folders to create nested navigation. ``` -content/ +my-docs/ ├── chronicle.yaml -├── index.mdx -├── getting-started.mdx -└── guides/ - ├── installation.mdx - └── configuration.mdx +├── content/ +│ ├── index.mdx +│ ├── getting-started.mdx +│ └── guides/ +│ ├── installation.mdx +│ └── configuration.mdx +└── .chronicle/ # generated, gitignored ``` ### 4. Build for production @@ -68,9 +78,11 @@ A minimal Chronicle project looks like: ``` my-docs/ ├── chronicle.yaml # Site configuration -├── index.mdx # Home page -└── guides/ - └── setup.mdx # Nested page at /guides/setup +├── content/ +│ ├── index.mdx # Home page +│ └── guides/ +│ └── setup.mdx # Nested page at /guides/setup +└── .chronicle/ # Generated by init, gitignored ``` All configuration is done through `chronicle.yaml`. No additional config files needed. diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index 970edbd..6f508a7 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -10,7 +10,8 @@ "src", "templates", "next.config.mjs", - "source.config.ts" + "source.config.ts", + "tsconfig.json" ], "bin": { "chronicle": "./bin/chronicle.js" @@ -27,7 +28,6 @@ "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", - "openapi-types": "^12.1.3", "semver": "^7.7.4", "typescript": "5.9.3" }, @@ -56,6 +56,7 @@ "slugify": "^1.6.6", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", + "openapi-types": "^12.1.3", "yaml": "^2.8.2", "zod": "^4.3.6" } diff --git a/packages/chronicle/src/cli/commands/init.ts b/packages/chronicle/src/cli/commands/init.ts index a6f8422..f9baee3 100644 --- a/packages/chronicle/src/cli/commands/init.ts +++ b/packages/chronicle/src/cli/commands/init.ts @@ -52,18 +52,13 @@ This is your documentation home page. export const initCommand = new Command('init') .description('Initialize a new Chronicle project') - .action(() => { + .option('-c, --content ', 'Content directory name', 'content') + .action((options) => { const projectDir = process.cwd() const dirName = path.basename(projectDir) || 'docs' - const contentDir = path.join(projectDir, 'content') + const contentDir = path.join(projectDir, options.content) - // Create project directory - if (!fs.existsSync(projectDir)) { - fs.mkdirSync(projectDir, { recursive: true }) - console.log(chalk.green('✓'), 'Created', projectDir) - } - - // Create content directory + // Create content directory if it doesn't exist if (!fs.existsSync(contentDir)) { fs.mkdirSync(contentDir, { recursive: true }) console.log(chalk.green('✓'), 'Created', contentDir) @@ -87,9 +82,10 @@ export const initCommand = new Command('init') console.log(chalk.yellow('⚠'), configPath, 'already exists') } - // Create sample index.mdx in content/ - const indexPath = path.join(contentDir, 'index.mdx') - if (!fs.existsSync(indexPath)) { + // Create sample index.mdx only if content dir is empty + const contentFiles = fs.readdirSync(contentDir) + if (contentFiles.length === 0) { + const indexPath = path.join(contentDir, 'index.mdx') fs.writeFileSync(indexPath, sampleMdx) console.log(chalk.green('✓'), 'Created', indexPath) } diff --git a/packages/chronicle/tsconfig.json b/packages/chronicle/tsconfig.json index 471ded2..4512ea8 100644 --- a/packages/chronicle/tsconfig.json +++ b/packages/chronicle/tsconfig.json @@ -1,6 +1,20 @@ { - "extends": "@raystack/tools-config/tsconfig/react", "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "module": "ESNext", + "target": "es6", "outDir": "dist", "rootDir": ".", "baseUrl": ".", From f94fdd7ab15a45fd26c95003d98bea4cfd1581ec Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 10:50:50 +0530 Subject: [PATCH 11/16] fix: resolve config from project root and fix .source import path Pass CHRONICLE_PROJECT_ROOT env var to Next.js child process so runtime config loader finds chronicle.yaml at the actual project root instead of .chronicle/ scaffold dir. Use tsconfig path alias for .source/server import to resolve correctly in scaffold context. Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/cli/commands/build.ts | 1 + packages/chronicle/src/cli/commands/dev.ts | 1 + packages/chronicle/src/cli/commands/serve.ts | 1 + packages/chronicle/src/cli/commands/start.ts | 1 + packages/chronicle/src/lib/config.ts | 8 ++++++++ packages/chronicle/src/lib/source.ts | 2 +- packages/chronicle/tsconfig.json | 2 +- 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/chronicle/src/cli/commands/build.ts b/packages/chronicle/src/cli/commands/build.ts index f352a8c..3796fa2 100644 --- a/packages/chronicle/src/cli/commands/build.ts +++ b/packages/chronicle/src/cli/commands/build.ts @@ -23,6 +23,7 @@ export const buildCommand = new Command('build') cwd: scaffoldPath, env: { ...process.env, + CHRONICLE_PROJECT_ROOT: process.cwd(), CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/cli/commands/dev.ts b/packages/chronicle/src/cli/commands/dev.ts index bfcf092..8a482e8 100644 --- a/packages/chronicle/src/cli/commands/dev.ts +++ b/packages/chronicle/src/cli/commands/dev.ts @@ -24,6 +24,7 @@ export const devCommand = new Command('dev') cwd: scaffoldPath, env: { ...process.env, + CHRONICLE_PROJECT_ROOT: process.cwd(), CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/cli/commands/serve.ts b/packages/chronicle/src/cli/commands/serve.ts index 20ea057..e6f2992 100644 --- a/packages/chronicle/src/cli/commands/serve.ts +++ b/packages/chronicle/src/cli/commands/serve.ts @@ -19,6 +19,7 @@ export const serveCommand = new Command('serve') const env = { ...process.env, + CHRONICLE_PROJECT_ROOT: process.cwd(), CHRONICLE_CONTENT_DIR: './content', } diff --git a/packages/chronicle/src/cli/commands/start.ts b/packages/chronicle/src/cli/commands/start.ts index 7975df1..f95fd61 100644 --- a/packages/chronicle/src/cli/commands/start.ts +++ b/packages/chronicle/src/cli/commands/start.ts @@ -24,6 +24,7 @@ export const startCommand = new Command('start') cwd: scaffoldPath, env: { ...process.env, + CHRONICLE_PROJECT_ROOT: process.cwd(), CHRONICLE_CONTENT_DIR: './content', }, }) diff --git a/packages/chronicle/src/lib/config.ts b/packages/chronicle/src/lib/config.ts index 74144dc..a0aa834 100644 --- a/packages/chronicle/src/lib/config.ts +++ b/packages/chronicle/src/lib/config.ts @@ -12,8 +12,16 @@ const defaultConfig: ChronicleConfig = { } function resolveConfigPath(): string | null { + // Check project root via env var + const projectRoot = process.env.CHRONICLE_PROJECT_ROOT + if (projectRoot) { + const rootPath = path.join(projectRoot, CONFIG_FILE) + if (fs.existsSync(rootPath)) return rootPath + } + // Check cwd const cwdPath = path.join(process.cwd(), CONFIG_FILE) if (fs.existsSync(cwdPath)) return cwdPath + // Check content dir const contentDir = process.env.CHRONICLE_CONTENT_DIR if (contentDir) { const contentPath = path.join(contentDir, CONFIG_FILE) diff --git a/packages/chronicle/src/lib/source.ts b/packages/chronicle/src/lib/source.ts index 3acd459..71cb663 100644 --- a/packages/chronicle/src/lib/source.ts +++ b/packages/chronicle/src/lib/source.ts @@ -1,4 +1,4 @@ -import { docs } from '../../.source/server' +import { docs } from '@/.source/server' import { loader } from 'fumadocs-core/source' import type { PageTree, PageTreeItem, Frontmatter } from '@/types' diff --git a/packages/chronicle/tsconfig.json b/packages/chronicle/tsconfig.json index 4512ea8..b21ec05 100644 --- a/packages/chronicle/tsconfig.json +++ b/packages/chronicle/tsconfig.json @@ -22,7 +22,7 @@ "moduleResolution": "bundler", "paths": { "@/*": ["./src/*"], - "@/.source": ["./.source/index"] + "@/.source/*": ["./.source/*"] } }, "include": ["src", ".source", "source.config.ts"], From 6b53016e5634c965a0af6c4dfa922c6ddba8f5e1 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 11:54:13 +0530 Subject: [PATCH 12/16] fix: merge missing scripts and deps into existing package.json on init Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/cli/commands/init.ts | 40 +++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/chronicle/src/cli/commands/init.ts b/packages/chronicle/src/cli/commands/init.ts index f9baee3..56db1bf 100644 --- a/packages/chronicle/src/cli/commands/init.ts +++ b/packages/chronicle/src/cli/commands/init.ts @@ -64,13 +64,49 @@ export const initCommand = new Command('init') console.log(chalk.green('✓'), 'Created', contentDir) } - // Create package.json in project root + // Create or update package.json in project root const packageJsonPath = path.join(projectDir, 'package.json') if (!fs.existsSync(packageJsonPath)) { fs.writeFileSync(packageJsonPath, JSON.stringify(createPackageJson(dirName), null, 2) + '\n') console.log(chalk.green('✓'), 'Created', packageJsonPath) } else { - console.log(chalk.yellow('⚠'), packageJsonPath, 'already exists') + const existing = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + const template = createPackageJson(dirName) + let updated = false + + // Merge missing scripts + if (!existing.scripts) existing.scripts = {} + for (const [key, value] of Object.entries(template.scripts as Record)) { + if (!existing.scripts[key]) { + existing.scripts[key] = value + updated = true + } + } + + // Merge missing dependencies + if (!existing.dependencies) existing.dependencies = {} + for (const [key, value] of Object.entries(template.dependencies as Record)) { + if (!existing.dependencies[key]) { + existing.dependencies[key] = value + updated = true + } + } + + // Merge missing devDependencies + if (!existing.devDependencies) existing.devDependencies = {} + for (const [key, value] of Object.entries(template.devDependencies as Record)) { + if (!existing.devDependencies[key]) { + existing.devDependencies[key] = value + updated = true + } + } + + if (updated) { + fs.writeFileSync(packageJsonPath, JSON.stringify(existing, null, 2) + '\n') + console.log(chalk.green('✓'), 'Updated', packageJsonPath, 'with missing scripts/deps') + } else { + console.log(chalk.yellow('⚠'), packageJsonPath, 'already has all required entries') + } } // Create chronicle.yaml in project root From 5a2be794e5688d420648ccdae33cb7335c59c36b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 11:54:42 +0530 Subject: [PATCH 13/16] chore: remove debug npm whoami step from canary workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index a0be34c..2b0c869 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -34,13 +34,6 @@ jobs: mv package.tmp.json package.json echo "Canary version: $VERSION" - - name: Verify npm auth - run: | - echo "//registry.npmjs.org/:_authToken=${NPM_CONFIG_TOKEN}" > ~/.npmrc - npm whoami - env: - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Publish run: bun publish --tag canary --access public env: From 30bf0df75e5299291e9628451f726be890c18167 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 13:28:32 +0530 Subject: [PATCH 14/16] fix: set type module and add node_modules/.next to gitignore in init Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/cli/commands/init.ts | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/chronicle/src/cli/commands/init.ts b/packages/chronicle/src/cli/commands/init.ts index 56db1bf..1109055 100644 --- a/packages/chronicle/src/cli/commands/init.ts +++ b/packages/chronicle/src/cli/commands/init.ts @@ -21,6 +21,7 @@ function createPackageJson(name: string): Record { return { name, private: true, + type: 'module', scripts: { dev: 'chronicle dev', build: 'chronicle build', @@ -74,6 +75,12 @@ export const initCommand = new Command('init') const template = createPackageJson(dirName) let updated = false + // Set type to module + if (existing.type !== 'module') { + existing.type = 'module' + updated = true + } + // Merge missing scripts if (!existing.scripts) existing.scripts = {} for (const [key, value] of Object.entries(template.scripts as Record)) { @@ -126,18 +133,19 @@ export const initCommand = new Command('init') console.log(chalk.green('✓'), 'Created', indexPath) } - // Add .chronicle to .gitignore + // Add entries to .gitignore const gitignorePath = path.join(projectDir, '.gitignore') - const chronicleEntry = '.chronicle' + const gitignoreEntries = ['.chronicle', 'node_modules', '.next'] if (fs.existsSync(gitignorePath)) { const existing = fs.readFileSync(gitignorePath, 'utf-8') - if (!existing.includes(chronicleEntry)) { - fs.appendFileSync(gitignorePath, `\n${chronicleEntry}\n`) - console.log(chalk.green('✓'), 'Added .chronicle to .gitignore') + const missing = gitignoreEntries.filter(e => !existing.includes(e)) + if (missing.length > 0) { + fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`) + console.log(chalk.green('✓'), 'Added', missing.join(', '), 'to .gitignore') } } else { - fs.writeFileSync(gitignorePath, `${chronicleEntry}\n`) - console.log(chalk.green('✓'), 'Created .gitignore with .chronicle') + fs.writeFileSync(gitignorePath, `${gitignoreEntries.join('\n')}\n`) + console.log(chalk.green('✓'), 'Created .gitignore') } // Install dependencies From 323385ab617c3824a71842a4eeae5c0edbd4e9e7 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 14:15:09 +0530 Subject: [PATCH 15/16] fix: render search highlights using dangerouslySetInnerHTML for mark tags Fumadocs returns search results with HTML tags for highlighting. Render them properly instead of showing raw HTML text. Co-Authored-By: Claude Opus 4.6 --- packages/chronicle/src/components/ui/search.module.css | 7 +++++++ packages/chronicle/src/components/ui/search.tsx | 10 +++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/chronicle/src/components/ui/search.module.css b/packages/chronicle/src/components/ui/search.module.css index 81ed6ac..086e7f4 100644 --- a/packages/chronicle/src/components/ui/search.module.css +++ b/packages/chronicle/src/components/ui/search.module.css @@ -102,3 +102,10 @@ .item[data-selected="true"] .icon { color: var(--rs-color-foreground-accent-primary-hover); } + +.pageText :global(mark), +.headingText :global(mark) { + background: transparent; + color: var(--rs-color-foreground-accent-primary); + font-weight: 600; +} diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index f028ddd..fb56aaa 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -111,7 +111,7 @@ export function Search({ className }: SearchProps) {
{getResultIcon(result)} - {stripMethod(result.content)} +
@@ -132,7 +132,7 @@ export function Search({ className }: SearchProps) { {result.type === "heading" ? ( <> - {stripMethod(result.content)} + - @@ -141,7 +141,7 @@ export function Search({ className }: SearchProps) { ) : ( - {stripMethod(result.content)} + )} @@ -178,6 +178,10 @@ function stripMethod(content: string): string { return API_METHODS.has(first) ? content.slice(first.length + 1) : content; } +function HighlightedText({ html, className }: { html: string; className?: string }) { + return ; +} + function getResultIcon(result: SortedResult): React.ReactNode { if (!result.url.startsWith("/apis/")) { return result.type === "page" ? ( From 0807b01c43ecb40b9a3c7c03c44aff06f4d308cc Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 13 Mar 2026 14:55:48 +0530 Subject: [PATCH 16/16] chore: remove canary release workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/canary.yml | 40 ------------------------------------ 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/canary.yml diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml deleted file mode 100644 index 2b0c869..0000000 --- a/.github/workflows/canary.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: canary - -on: - pull_request: - types: [opened, synchronize] - -jobs: - canary-release: - name: Publish canary to npm - runs-on: ubuntu-latest - timeout-minutes: 10 - defaults: - run: - working-directory: ./packages/chronicle - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: bun install --frozen-lockfile - working-directory: . - - - name: Build CLI - run: bun build-cli.ts - - - name: Set canary version - run: | - SHORT_SHA=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7) - VERSION=$(jq -r .version package.json)-canary.${SHORT_SHA} - jq --arg v "$VERSION" '.version = $v' package.json > package.tmp.json - mv package.tmp.json package.json - echo "Canary version: $VERSION" - - - name: Publish - run: bun publish --tag canary --access public - env: - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}