From f1524509312403c63ede025aff35f9b9cd380adb Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 30 Mar 2026 00:16:59 +0800 Subject: [PATCH] feat: add JSON config module with load/save/defaults Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 +- src/data/config.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++ src/data/config.ts | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/data/config.test.ts create mode 100644 src/data/config.ts diff --git a/.gitignore b/.gitignore index 7b66359..900d237 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules/ dist/ -data/ +/data/ *.db .env .superpowers/ diff --git a/src/data/config.test.ts b/src/data/config.test.ts new file mode 100644 index 0000000..fc398a4 --- /dev/null +++ b/src/data/config.test.ts @@ -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); + }); +}); diff --git a/src/data/config.ts b/src/data/config.ts new file mode 100644 index 0000000..e760dc0 --- /dev/null +++ b/src/data/config.ts @@ -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; + 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; + 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"); +}