refactor: make config more structured

This commit is contained in:
2026-06-28 22:43:08 +09:00
parent 1cebbf65eb
commit cb9582e80c
6 changed files with 83 additions and 52 deletions

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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
View 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
View 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
View 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;