feat: add content/preset support in chronicle.yaml with Zod validation#27
feat: add content/preset support in chronicle.yaml with Zod validation#27
Conversation
Add `content` and `preset` fields to chronicle.yaml config. CLI flags override YAML values (flag > yaml > default). Replace manual TypeScript interfaces with Zod schemas for runtime config validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR refactors the CLI configuration system to use centralized async validation with Zod schemas. It introduces Changes
Sequence DiagramsequenceDiagram
participant Cmd as CLI Command
participant Load as loadCLIConfig
participant Read as readConfig
participant Validate as validateConfig
participant Resolve as resolveContentDir/<br/>resolvePreset
participant Vite as createViteConfig
Cmd->>Load: loadCLIConfig(configPath, options)
Load->>Read: readConfig(configPath)
Read-->>Load: raw YAML string
Load->>Validate: validateConfig(raw, configPath)
Validate-->>Load: ChronicleConfig (typed)
Load->>Resolve: resolveContentDir(config, options.content)
Resolve-->>Load: contentDir
Load->>Resolve: resolvePreset(config, options.preset)
Resolve-->>Load: preset
Load-->>Cmd: CLIConfig {contentDir, configPath, preset}
Cmd->>Vite: createViteConfig(..., preset, configPath)
Vite-->>Cmd: Vite config
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/chronicle/src/cli/commands/start.ts (1)
7-12:⚠️ Potential issue | 🟡 MinorMissing
--configoption for consistency with other commands.The
startcommand is missing the--config <path>option thatdev,build, andserveall define. This means users cannot specify a custom config path when usingchronicle start, while they can for all other commands.Proposed fix to add --config option
export const startCommand = new Command('start') .description('Start production server') .option('-p, --port <port>', 'Port number', '3000') .option('--content <path>', 'Content directory') + .option('--config <path>', 'Path to chronicle.yaml') .action(async options => { - const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content }); + const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/chronicle/src/cli/commands/start.ts` around lines 7 - 12, The startCommand is missing the --config option; add .option('--config <path>', 'Config path') to the startCommand definition and pass the value into loadCLIConfig like the other CLI commands (i.e., call loadCLIConfig(undefined, { content: options.content, config: options.config }) so the existing destructure of { contentDir, configPath } receives the user-provided config path; reference startCommand and loadCLIConfig when making this change.
🧹 Nitpick comments (3)
packages/chronicle/src/cli/utils/config.ts (1)
19-30: Consider explicitneverreturn type annotation for clarity.The
.catch()handler callsprocess.exit(1)which never returns, but TypeScript may infer the return type asPromise<string | never>. While the runtime behavior is correct, adding an explicit return type annotation or usingthrowafter logging would make the control flow clearer.Alternative pattern using throw
async function readConfig(configPath: string): Promise<string> { - return fs.readFile(configPath, 'utf-8').catch((error: NodeJS.ErrnoException) => { + try { + return await fs.readFile(configPath, 'utf-8'); + } catch (err) { + const error = err as NodeJS.ErrnoException; if (error.code === 'ENOENT') { console.log(chalk.red(`Error: chronicle.yaml not found at '${configPath}'`)); console.log(chalk.gray("Run 'chronicle init' to create one")); } else { console.log(chalk.red(`Error: Failed to read '${configPath}'`)); console.log(chalk.gray(error.message)); } process.exit(1); - }); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/chronicle/src/cli/utils/config.ts` around lines 19 - 30, The readConfig function's Promise return type is ambiguous because the .catch callback calls process.exit(1); fix by making control flow explicit: update the readConfig signature to Promise<string> and annotate the .catch handler callback to return never (e.g., (error: NodeJS.ErrnoException): never => { ... process.exit(1) }), or alternatively rewrite readConfig to async/await with a try/catch that logs and then throws or calls process.exit, ensuring the catch block has an explicit never/throw so TypeScript infers Promise<string> for readConfig; reference symbols: readConfig, fs.readFile, .catch, process.exit.packages/chronicle/src/types/config.ts (2)
8-11: Consider makingtheme.nameoptional with a default.When
themeis provided in YAML,nameis required. This means a user who only wants to customizecolorsmust also specifyname:theme: colors: primary: "#ff0000" # This will fail validation because `name` is missingOption 1: Make name optional
const themeSchema = z.object({ - name: z.enum(['default', 'paper']), + name: z.enum(['default', 'paper']).optional(), colors: z.record(z.string(), z.string()).optional(), })Option 2: Use default value (Zod 4 syntax)
const themeSchema = z.object({ - name: z.enum(['default', 'paper']), + name: z.enum(['default', 'paper']).default('default'), colors: z.record(z.string(), z.string()).optional(), })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/chronicle/src/types/config.ts` around lines 8 - 11, The current themeSchema requires theme.name so users who only set theme.colors fail validation; update themeSchema so name is optional with a sensible default (e.g., "default") by changing the name field on themeSchema (the z.enum(['default','paper']) entry) to use Zod's optional/default API (e.g., make it optional and .default('default') or directly .default('default')), so that when an object includes theme.colors but omits name it validates and the resulting parsed value has name set to "default".
44-50: Consider makingapiSchema.serveroptional.The
serverfield is required when defining an API entry, but users might not always need to override server settings if the spec file contains server definitions. This could be overly restrictive.Proposed change
const apiSchema = z.object({ name: z.string(), spec: z.string(), basePath: z.string(), - server: apiServerSchema, + server: apiServerSchema.optional(), auth: apiAuthSchema.optional(), })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/chronicle/src/types/config.ts` around lines 44 - 50, The apiSchema currently requires a server field which can be too strict; update apiSchema to make server optional by replacing server: apiServerSchema with server: apiServerSchema.optional(), and adjust any derived TypeScript types or usages that assume server is always present (e.g., references to apiSchema, Api types, or code that accesses api.server) to handle undefined/null accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/chronicle/src/cli/commands/start.ts`:
- Around line 7-12: The startCommand is missing the --config option; add
.option('--config <path>', 'Config path') to the startCommand definition and
pass the value into loadCLIConfig like the other CLI commands (i.e., call
loadCLIConfig(undefined, { content: options.content, config: options.config })
so the existing destructure of { contentDir, configPath } receives the
user-provided config path; reference startCommand and loadCLIConfig when making
this change.
---
Nitpick comments:
In `@packages/chronicle/src/cli/utils/config.ts`:
- Around line 19-30: The readConfig function's Promise return type is ambiguous
because the .catch callback calls process.exit(1); fix by making control flow
explicit: update the readConfig signature to Promise<string> and annotate the
.catch handler callback to return never (e.g., (error: NodeJS.ErrnoException):
never => { ... process.exit(1) }), or alternatively rewrite readConfig to
async/await with a try/catch that logs and then throws or calls process.exit,
ensuring the catch block has an explicit never/throw so TypeScript infers
Promise<string> for readConfig; reference symbols: readConfig, fs.readFile,
.catch, process.exit.
In `@packages/chronicle/src/types/config.ts`:
- Around line 8-11: The current themeSchema requires theme.name so users who
only set theme.colors fail validation; update themeSchema so name is optional
with a sensible default (e.g., "default") by changing the name field on
themeSchema (the z.enum(['default','paper']) entry) to use Zod's
optional/default API (e.g., make it optional and .default('default') or directly
.default('default')), so that when an object includes theme.colors but omits
name it validates and the resulting parsed value has name set to "default".
- Around line 44-50: The apiSchema currently requires a server field which can
be too strict; update apiSchema to make server optional by replacing server:
apiServerSchema with server: apiServerSchema.optional(), and adjust any derived
TypeScript types or usages that assume server is always present (e.g.,
references to apiSchema, Api types, or code that accesses api.server) to handle
undefined/null accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 47eca8b8-1c3e-4d7e-8cc6-d58a0909a2b1
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
examples/basic/chronicle.yamlpackages/chronicle/package.jsonpackages/chronicle/src/cli/commands/build.tspackages/chronicle/src/cli/commands/dev.tspackages/chronicle/src/cli/commands/serve.tspackages/chronicle/src/cli/commands/start.tspackages/chronicle/src/cli/utils/config.tspackages/chronicle/src/types/config.ts
Summary
contentandpresetfields tochronicle.yamlconfig schema--content,--preset) override YAML values (flag > yaml > default)readConfig(I/O) andvalidateConfig(Zod safeParse) with user-friendly error messagesTest plan
contentfield in YAML resolves content directory correctlypresetfield in YAML is used by build/serve commands🤖 Generated with Claude Code