Skip to content

Commit ec1599a

Browse files
rsbhclaude
andauthored
feat: add --config flag for chronicle.yaml path (#26)
* fix: resolve chronicle.yaml only from cwd, not content dir Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add --config flag and remove -c short arg from CLI commands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: throw error when explicit --config path is unreadable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename configFlag to configPath and return undefined instead of null Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: use fs promises and simplify resolveConfigPath Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract readConfig function with separate error handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c3b2b6 commit ec1599a

6 files changed

Lines changed: 52 additions & 36 deletions

File tree

packages/chronicle/src/cli/commands/build.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import chalk from 'chalk';
22
import { Command } from 'commander';
3-
import { resolveContentDir } from '@/cli/utils/config';
3+
import { resolveConfigPath, resolveContentDir } from '@/cli/utils/config';
44
import { PACKAGE_ROOT } from '@/cli/utils/resolve';
55
import { linkContent } from '@/cli/utils/scaffold';
66

77
export const buildCommand = new Command('build')
88
.description('Build for production')
9-
.option('-c, --content <path>', 'Content directory')
9+
.option('--content <path>', 'Content directory')
10+
.option('--config <path>', 'Path to chronicle.yaml')
1011
.option(
1112
'--preset <preset>',
1213
'Deploy preset (vercel, cloudflare, node-server)'
1314
)
1415
.action(async options => {
1516
const contentDir = resolveContentDir(options.content);
17+
const configPath = resolveConfigPath(options.config);
1618
await linkContent(contentDir);
1719

1820
console.log(chalk.cyan('Building for production...'));
@@ -24,6 +26,7 @@ export const buildCommand = new Command('build')
2426
packageRoot: PACKAGE_ROOT,
2527
projectRoot: process.cwd(),
2628
contentDir,
29+
configPath,
2730
preset: options.preset
2831
});
2932

packages/chronicle/src/cli/commands/dev.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import chalk from 'chalk';
44
import { Command } from 'commander';
5-
import { resolveContentDir } from '@/cli/utils/config';
5+
import { resolveConfigPath, resolveContentDir } from '@/cli/utils/config';
66
import { PACKAGE_ROOT } from '@/cli/utils/resolve';
77
import { linkContent } from '@/cli/utils/scaffold';
88

99
export const devCommand = new Command('dev')
1010
.description('Start development server')
1111
.option('-p, --port <port>', 'Port number', '3000')
12-
.option('-c, --content <path>', 'Content directory')
12+
.option('--content <path>', 'Content directory')
13+
.option('--config <path>', 'Path to chronicle.yaml')
1314
.action(async options => {
1415
const contentDir = resolveContentDir(options.content);
16+
const configPath = resolveConfigPath(options.config);
1517
const port = parseInt(options.port, 10);
1618

1719
await linkContent(contentDir);
@@ -21,7 +23,7 @@ export const devCommand = new Command('dev')
2123
const { createServer } = await import('vite');
2224
const { createViteConfig } = await import('@/server/vite-config');
2325

24-
const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir });
26+
const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
2527
const server = await createServer({
2628
...config,
2729
server: { ...config.server, port }

packages/chronicle/src/cli/commands/serve.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import chalk from 'chalk';
22
import { Command } from 'commander';
3-
import { resolveContentDir } from '@/cli/utils/config';
3+
import { resolveConfigPath, resolveContentDir } from '@/cli/utils/config';
44
import { PACKAGE_ROOT } from '@/cli/utils/resolve';
55
import { linkContent } from '@/cli/utils/scaffold';
66

77
export const serveCommand = new Command('serve')
88
.description('Build and start production server')
99
.option('-p, --port <port>', 'Port number', '3000')
10-
.option('-c, --content <path>', 'Content directory')
10+
.option('--content <path>', 'Content directory')
11+
.option('--config <path>', 'Path to chronicle.yaml')
1112
.option(
1213
'--preset <preset>',
1314
'Deploy preset (vercel, cloudflare, node-server)'
1415
)
1516
.action(async options => {
1617
const contentDir = resolveContentDir(options.content);
18+
const configPath = resolveConfigPath(options.config);
1719
const port = parseInt(options.port, 10);
1820
await linkContent(contentDir);
1921

@@ -24,6 +26,7 @@ export const serveCommand = new Command('serve')
2426
packageRoot: PACKAGE_ROOT,
2527
projectRoot: process.cwd(),
2628
contentDir,
29+
configPath,
2730
preset: options.preset
2831
});
2932

packages/chronicle/src/cli/commands/start.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { linkContent } from '@/cli/utils/scaffold';
77
export const startCommand = new Command('start')
88
.description('Start production server')
99
.option('-p, --port <port>', 'Port number', '3000')
10-
.option('-c, --content <path>', 'Content directory')
10+
.option('--content <path>', 'Content directory')
1111
.action(async options => {
1212
const contentDir = resolveContentDir(options.content);
1313
const port = parseInt(options.port, 10);
Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from 'node:fs';
1+
import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import chalk from 'chalk';
44
import { parse } from 'yaml';
@@ -15,28 +15,32 @@ export function resolveContentDir(contentFlag?: string): string {
1515
return path.resolve('content');
1616
}
1717

18-
function resolveConfigPath(contentDir: string): string | null {
19-
const cwdPath = path.join(process.cwd(), 'chronicle.yaml');
20-
if (fs.existsSync(cwdPath)) return cwdPath;
21-
const contentPath = path.join(contentDir, 'chronicle.yaml');
22-
if (fs.existsSync(contentPath)) return contentPath;
23-
return null;
18+
export function resolveConfigPath(configPath?: string): string | undefined {
19+
if (configPath) return path.resolve(configPath);
20+
return undefined;
2421
}
2522

26-
export function loadCLIConfig(contentDir: string): CLIConfig {
27-
const configPath = resolveConfigPath(contentDir);
28-
29-
if (!configPath) {
30-
console.log(
31-
chalk.red(
32-
`Error: chronicle.yaml not found in '${process.cwd()}' or '${contentDir}'`
33-
)
34-
);
35-
console.log(chalk.gray("Run 'chronicle init' to create one"));
23+
async function readConfig(configPath: string): Promise<ChronicleConfig> {
24+
try {
25+
const raw = await fs.readFile(configPath, 'utf-8');
26+
return parse(raw) as ChronicleConfig;
27+
} catch (error) {
28+
const err = error as NodeJS.ErrnoException;
29+
if (err.code === 'ENOENT') {
30+
console.log(chalk.red(`Error: chronicle.yaml not found at '${configPath}'`));
31+
console.log(chalk.gray("Run 'chronicle init' to create one"));
32+
} else {
33+
console.log(chalk.red(`Error: Invalid YAML in '${configPath}'`));
34+
console.log(chalk.gray(err.message));
35+
}
3636
process.exit(1);
3737
}
38+
}
3839

39-
const config = parse(fs.readFileSync(configPath, 'utf-8')) as ChronicleConfig;
40+
export async function loadCLIConfig(contentDir: string, configPath?: string): Promise<CLIConfig> {
41+
const resolvedConfigPath = resolveConfigPath(configPath)
42+
?? path.join(process.cwd(), 'chronicle.yaml');
4043

41-
return { config, configPath, contentDir };
44+
const config = await readConfig(resolvedConfigPath);
45+
return { config, configPath: resolvedConfigPath, contentDir };
4246
}

packages/chronicle/src/server/vite-config.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,30 @@ export interface ViteConfigOptions {
1919
packageRoot: string;
2020
projectRoot: string;
2121
contentDir: string;
22+
configPath?: string;
2223
preset?: string;
2324
}
2425

25-
async function readChronicleConfig(projectRoot: string, contentDir: string): Promise<string | null> {
26-
for (const dir of [projectRoot, contentDir]) {
27-
const filePath = path.join(dir, 'chronicle.yaml');
26+
async function readChronicleConfig(projectRoot: string, configPath?: string): Promise<string | null> {
27+
if (configPath) {
2828
try {
29-
return await fs.readFile(filePath, 'utf-8');
30-
} catch {
31-
// not found, try next
29+
return await fs.readFile(configPath, 'utf-8');
30+
} catch (error) {
31+
throw new Error(`Failed to read config file '${configPath}': ${(error as Error).message}`);
3232
}
3333
}
34-
return null;
34+
try {
35+
return await fs.readFile(path.join(projectRoot, 'chronicle.yaml'), 'utf-8');
36+
} catch {
37+
return null;
38+
}
3539
}
3640

3741
export async function createViteConfig(
3842
options: ViteConfigOptions
3943
): Promise<InlineConfig> {
40-
const { packageRoot, projectRoot, contentDir, preset } = options;
41-
const rawConfig = await readChronicleConfig(projectRoot, contentDir);
44+
const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
45+
const rawConfig = await readChronicleConfig(projectRoot, configPath);
4246

4347
return {
4448
root: packageRoot,

0 commit comments

Comments
 (0)