feat: add JSON config module with load/save/defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 00:16:59 +08:00
1 parent 101d593c0e
commit f152450931
3 files changed
+104 -1

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
node_modules/ node_modules/
dist/ dist/
data/ /data/
*.db *.db
.env .env
.superpowers/ .superpowers/
+55
View File
@@ -0,0 +1,55 @@
import { describe, it, expect, afterEach } from "vitest";
import { join } from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { getDefaultConfig, loadConfig, saveConfig } from "./config.js";
describe("config", () => {
const dirs: string[] = [];
function makeTmpDir(): string {
const dir = mkdtempSync(join(tmpdir(), "tsmusicbot-test-"));
dirs.push(dir);
return dir;
}
afterEach(() => {
for (const d of dirs) {
rmSync(d, { recursive: true, force: true });
}
dirs.length = 0;
});
it("returns default config when file does not exist", () => {
const config = loadConfig("/nonexistent/path/config.json");
expect(config).toEqual(getDefaultConfig());
});
it("creates config file on save", () => {
const dir = makeTmpDir();
const path = join(dir, "sub", "config.json");
const config = getDefaultConfig();
saveConfig(path, config);
const loaded = loadConfig(path);
expect(loaded).toEqual(config);
});
it("merges partial config with defaults", () => {
const dir = makeTmpDir();
const path = join(dir, "config.json");
// Save a partial config by writing only some fields
const partial = { webPort: 8080, locale: "en" };
const { writeFileSync } = require("node:fs");
writeFileSync(path, JSON.stringify(partial), "utf-8");
const loaded = loadConfig(path);
expect(loaded.webPort).toBe(8080);
expect(loaded.locale).toBe("en");
// defaults should fill in the rest
expect(loaded.theme).toBe("dark");
expect(loaded.commandPrefix).toBe("!");
expect(loaded.autoPauseOnEmpty).toBe(true);
});
});
+48
View File
@@ -0,0 +1,48 @@
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
export interface BotConfig {
webPort: number;
locale: string;
theme: string;
commandPrefix: string;
commandAliases: Record<string, string>;
neteaseApiPort: number;
qqMusicApiPort: number;
adminPassword: string;
adminGroups: number[];
autoReturnDelay: number;
autoPauseOnEmpty: boolean;
}
export function getDefaultConfig(): BotConfig {
return {
webPort: 3000,
locale: "zh",
theme: "dark",
commandPrefix: "!",
commandAliases: { p: "play", s: "skip", n: "next" },
neteaseApiPort: 3001,
qqMusicApiPort: 3002,
adminPassword: "",
adminGroups: [],
autoReturnDelay: 300,
autoPauseOnEmpty: true,
};
}
export function loadConfig(path: string): BotConfig {
const defaults = getDefaultConfig();
try {
const raw = readFileSync(path, "utf-8");
const partial = JSON.parse(raw) as Partial<BotConfig>;
return { ...defaults, ...partial };
} catch {
return defaults;
}
}
export function saveConfig(path: string, config: BotConfig): void {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
}