refactor: make config more structured
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -11,6 +11,7 @@
|
||||
"prettier": "^3.8.3",
|
||||
"supermemory": "^4.24.12",
|
||||
"yaml": "^2.9.0",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.9.1",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"commander": "^15.0.0",
|
||||
"prettier": "^3.8.3",
|
||||
"supermemory": "^4.24.12",
|
||||
"yaml": "^2.9.0"
|
||||
"yaml": "^2.9.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
export interface Config {
|
||||
openrouterApiKey: string;
|
||||
supermemoryApiKey: string;
|
||||
brainboxRoot: string;
|
||||
}
|
||||
|
||||
const brainboxRoot = process.env["BRAINBOX_ROOT_PATH"]
|
||||
? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"])
|
||||
: join(homedir(), ".brainbox");
|
||||
|
||||
interface BrainboxYaml {
|
||||
openrouter?: { apiKey?: string };
|
||||
supermemory?: { apiKey?: string };
|
||||
}
|
||||
|
||||
const yamlPath = join(brainboxRoot, "brainbox.yaml");
|
||||
let parsed: BrainboxYaml = {};
|
||||
try {
|
||||
parsed = parseYaml(readFileSync(yamlPath, "utf8")) ?? {};
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
mkdirSync(dirname(yamlPath), { recursive: true });
|
||||
writeFileSync(
|
||||
yamlPath,
|
||||
"# Fill in your API keys, then run brainbox again.\n" +
|
||||
"openrouter:\n" +
|
||||
" apiKey: \n" +
|
||||
"supermemory:\n" +
|
||||
" apiKey: \n",
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const openrouterApiKey = parsed.openrouter?.apiKey;
|
||||
if (!openrouterApiKey) throw new Error(`openrouter.apiKey is missing in ${yamlPath}`);
|
||||
|
||||
const supermemoryApiKey = parsed.supermemory?.apiKey;
|
||||
if (!supermemoryApiKey) throw new Error(`supermemory.apiKey is missing in ${yamlPath}`);
|
||||
|
||||
export const config: Config = {
|
||||
openrouterApiKey,
|
||||
supermemoryApiKey,
|
||||
brainboxRoot,
|
||||
};
|
||||
14
src/config/index.ts
Normal file
14
src/config/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { brainboxRoot } from "./loader";
|
||||
import rootConfig from "./root";
|
||||
|
||||
export interface Config {
|
||||
openrouterApiKey: string;
|
||||
supermemoryApiKey: string;
|
||||
brainboxRoot: string;
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
brainboxRoot,
|
||||
openrouterApiKey: rootConfig.openrouter.apiKey,
|
||||
supermemoryApiKey: rootConfig.supermemory.apiKey,
|
||||
};
|
||||
45
src/config/loader.ts
Normal file
45
src/config/loader.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { z, ZodError } from "zod";
|
||||
import { homedir } from "os";
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
|
||||
export const brainboxRoot = process.env["BRAINBOX_ROOT_PATH"]
|
||||
? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"])
|
||||
: join(homedir(), ".brainbox");
|
||||
|
||||
// ponytail: add a file → define a zod schema + one parseConfigFile() call.
|
||||
// ponytail: add a key → extend the schema, extend the template.body, add to Config + the mapping below.
|
||||
// ponytail: keep template.body and the schema in sync by hand; replace with z.toJSONSchema → defaults when more than ~5 files.
|
||||
export function parseConfigFile<T>(
|
||||
file: string,
|
||||
template: { header?: string; body: Record<string, unknown> },
|
||||
schema: z.ZodType<T>,
|
||||
): T {
|
||||
const path = join(brainboxRoot, file);
|
||||
const templateStr =
|
||||
(template.header ? template.header + "\n" : "") +
|
||||
stringifyYaml(template.body);
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = parseYaml(readFileSync(path, "utf8")) ?? {};
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, templateStr);
|
||||
raw = {};
|
||||
}
|
||||
try {
|
||||
return schema.parse(raw);
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
throw new Error(
|
||||
`Invalid config in ${path}:\n` +
|
||||
err.issues
|
||||
.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
21
src/config/root.ts
Normal file
21
src/config/root.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import z from "zod";
|
||||
import { parseConfigFile } from "./loader";
|
||||
|
||||
const RootConfigSchema = z.object({
|
||||
openrouter: z.object({ apiKey: z.string().min(1) }),
|
||||
supermemory: z.object({ apiKey: z.string().min(1) }),
|
||||
});
|
||||
|
||||
const rootConfig = parseConfigFile(
|
||||
"brainbox.yaml",
|
||||
{
|
||||
header: "# Fill in your API keys, then run brainbox again.",
|
||||
body: {
|
||||
openrouter: { apiKey: "" },
|
||||
supermemory: { apiKey: "" },
|
||||
},
|
||||
},
|
||||
RootConfigSchema,
|
||||
);
|
||||
|
||||
export default rootConfig;
|
||||
Reference in New Issue
Block a user