packages/cli/src/lib/config.ts 3.3 K raw
1
import * as path from "path";
2
import type { PublisherConfig, PublisherState, FrontmatterMapping } from "./types";
3
4
const CONFIG_FILENAME = "sequoia.json";
5
const STATE_FILENAME = ".sequoia-state.json";
6
7
export async function findConfig(
8
	startDir: string = process.cwd(),
9
): Promise<string | null> {
10
	let currentDir = startDir;
11
12
	while (true) {
13
		const configPath = path.join(currentDir, CONFIG_FILENAME);
14
		const file = Bun.file(configPath);
15
16
		if (await file.exists()) {
17
			return configPath;
18
		}
19
20
		const parentDir = path.dirname(currentDir);
21
		if (parentDir === currentDir) {
22
			// Reached root
23
			return null;
24
		}
25
		currentDir = parentDir;
26
	}
27
}
28
29
export async function loadConfig(
30
	configPath?: string,
31
): Promise<PublisherConfig> {
32
	const resolvedPath = configPath || (await findConfig());
33
34
	if (!resolvedPath) {
35
		throw new Error(
36
			`Could not find ${CONFIG_FILENAME}. Run 'sequoia init' to create one.`,
37
		);
38
	}
39
40
	try {
41
		const file = Bun.file(resolvedPath);
42
		const content = await file.text();
43
		const config = JSON.parse(content) as PublisherConfig;
44
45
		// Validate required fields
46
		if (!config.siteUrl) throw new Error("siteUrl is required in config");
47
		if (!config.contentDir) throw new Error("contentDir is required in config");
48
		if (!config.publicationUri)
49
			throw new Error("publicationUri is required in config");
50
51
		return config;
52
	} catch (error) {
53
		if (error instanceof Error && error.message.includes("required")) {
54
			throw error;
55
		}
56
		throw new Error(`Failed to load config from ${resolvedPath}: ${error}`);
57
	}
58
}
59
60
export function generateConfigTemplate(options: {
61
	siteUrl: string;
62
	contentDir: string;
63
	imagesDir?: string;
64
	publicDir?: string;
65
	outputDir?: string;
66
	pathPrefix?: string;
67
	publicationUri: string;
68
	pdsUrl?: string;
69
	frontmatter?: FrontmatterMapping;
70
	ignore?: string[];
71
}): string {
72
	const config: Record<string, unknown> = {
73
		siteUrl: options.siteUrl,
74
		contentDir: options.contentDir,
75
	};
76
77
	if (options.imagesDir) {
78
		config.imagesDir = options.imagesDir;
79
	}
80
81
	if (options.publicDir && options.publicDir !== "./public") {
82
		config.publicDir = options.publicDir;
83
	}
84
85
	if (options.outputDir) {
86
		config.outputDir = options.outputDir;
87
	}
88
89
	if (options.pathPrefix && options.pathPrefix !== "/posts") {
90
		config.pathPrefix = options.pathPrefix;
91
	}
92
93
	config.publicationUri = options.publicationUri;
94
95
	if (options.pdsUrl && options.pdsUrl !== "https://bsky.social") {
96
		config.pdsUrl = options.pdsUrl;
97
	}
98
99
	if (options.frontmatter && Object.keys(options.frontmatter).length > 0) {
100
		config.frontmatter = options.frontmatter;
101
	}
102
103
	if (options.ignore && options.ignore.length > 0) {
104
		config.ignore = options.ignore;
105
	}
106
107
	return JSON.stringify(config, null, 2);
108
}
109
110
export async function loadState(configDir: string): Promise<PublisherState> {
111
	const statePath = path.join(configDir, STATE_FILENAME);
112
	const file = Bun.file(statePath);
113
114
	if (!(await file.exists())) {
115
		return { posts: {} };
116
	}
117
118
	try {
119
		const content = await file.text();
120
		return JSON.parse(content) as PublisherState;
121
	} catch {
122
		return { posts: {} };
123
	}
124
}
125
126
export async function saveState(
127
	configDir: string,
128
	state: PublisherState,
129
): Promise<void> {
130
	const statePath = path.join(configDir, STATE_FILENAME);
131
	await Bun.write(statePath, JSON.stringify(state, null, 2));
132
}
133
134
export function getStatePath(configDir: string): string {
135
	return path.join(configDir, STATE_FILENAME);
136
}