121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { paperclipConfigSchema, type PaperclipConfig } from "./schema.js";
|
|
import {
|
|
resolveDefaultConfigPath,
|
|
resolvePaperclipInstanceId,
|
|
} from "./home.js";
|
|
|
|
const DEFAULT_CONFIG_BASENAME = "config.json";
|
|
|
|
function findConfigFileFromAncestors(startDir: string): string | null {
|
|
const absoluteStartDir = path.resolve(startDir);
|
|
let currentDir = absoluteStartDir;
|
|
|
|
while (true) {
|
|
const candidate = path.resolve(currentDir, ".paperclip", DEFAULT_CONFIG_BASENAME);
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
|
|
const nextDir = path.resolve(currentDir, "..");
|
|
if (nextDir === currentDir) break;
|
|
currentDir = nextDir;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function resolveConfigPath(overridePath?: string): string {
|
|
if (overridePath) return path.resolve(overridePath);
|
|
if (process.env.PAPERCLIP_CONFIG) return path.resolve(process.env.PAPERCLIP_CONFIG);
|
|
return findConfigFileFromAncestors(process.cwd()) ?? resolveDefaultConfigPath(resolvePaperclipInstanceId());
|
|
}
|
|
|
|
function parseJson(filePath: string): unknown {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
} catch (err) {
|
|
throw new Error(`Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
}
|
|
|
|
function migrateLegacyConfig(raw: unknown): unknown {
|
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
const config = { ...(raw as Record<string, unknown>) };
|
|
const databaseRaw = config.database;
|
|
if (typeof databaseRaw !== "object" || databaseRaw === null || Array.isArray(databaseRaw)) {
|
|
return config;
|
|
}
|
|
|
|
const database = { ...(databaseRaw as Record<string, unknown>) };
|
|
if (database.mode === "pglite") {
|
|
database.mode = "embedded-postgres";
|
|
|
|
if (typeof database.embeddedPostgresDataDir !== "string" && typeof database.pgliteDataDir === "string") {
|
|
database.embeddedPostgresDataDir = database.pgliteDataDir;
|
|
}
|
|
if (
|
|
typeof database.embeddedPostgresPort !== "number" &&
|
|
typeof database.pglitePort === "number" &&
|
|
Number.isFinite(database.pglitePort)
|
|
) {
|
|
database.embeddedPostgresPort = database.pglitePort;
|
|
}
|
|
}
|
|
|
|
config.database = database;
|
|
return config;
|
|
}
|
|
|
|
function formatValidationError(err: unknown): string {
|
|
const issues = (err as { issues?: Array<{ path?: unknown; message?: unknown }> })?.issues;
|
|
if (Array.isArray(issues) && issues.length > 0) {
|
|
return issues
|
|
.map((issue) => {
|
|
const pathParts = Array.isArray(issue.path) ? issue.path.map(String) : [];
|
|
const issuePath = pathParts.length > 0 ? pathParts.join(".") : "config";
|
|
const message = typeof issue.message === "string" ? issue.message : "Invalid value";
|
|
return `${issuePath}: ${message}`;
|
|
})
|
|
.join("; ");
|
|
}
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|
|
|
|
export function readConfig(configPath?: string): PaperclipConfig | null {
|
|
const filePath = resolveConfigPath(configPath);
|
|
if (!fs.existsSync(filePath)) return null;
|
|
const raw = parseJson(filePath);
|
|
const migrated = migrateLegacyConfig(raw);
|
|
const parsed = paperclipConfigSchema.safeParse(migrated);
|
|
if (!parsed.success) {
|
|
throw new Error(`Invalid config at ${filePath}: ${formatValidationError(parsed.error)}`);
|
|
}
|
|
return parsed.data;
|
|
}
|
|
|
|
export function writeConfig(
|
|
config: PaperclipConfig,
|
|
configPath?: string,
|
|
): void {
|
|
const filePath = resolveConfigPath(configPath);
|
|
const dir = path.dirname(filePath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
|
|
// Backup existing config before overwriting
|
|
if (fs.existsSync(filePath)) {
|
|
const backupPath = filePath + ".backup";
|
|
fs.copyFileSync(filePath, backupPath);
|
|
fs.chmodSync(backupPath, 0o600);
|
|
}
|
|
|
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", {
|
|
mode: 0o600,
|
|
});
|
|
}
|
|
|
|
export function configExists(configPath?: string): boolean {
|
|
return fs.existsSync(resolveConfigPath(configPath));
|
|
}
|