import { existsSync, readFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { formatDatabaseBackupResult, runDatabaseBackup } from "./backup-lib.js"; type PartialConfig = { database?: { mode?: "embedded-postgres" | "postgres"; connectionString?: string; embeddedPostgresPort?: number; backup?: { dir?: string; retentionDays?: number; }; }; }; function expandHomePrefix(value: string): string { if (value === "~") return os.homedir(); if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2)); return value; } function resolvePaperclipHomeDir(): string { const envHome = process.env.PAPERCLIP_HOME?.trim(); if (envHome) return path.resolve(expandHomePrefix(envHome)); return path.resolve(os.homedir(), ".paperclip"); } function resolvePaperclipInstanceId(): string { const raw = process.env.PAPERCLIP_INSTANCE_ID?.trim() || "default"; if (!/^[a-zA-Z0-9_-]+$/.test(raw)) { throw new Error(`Invalid PAPERCLIP_INSTANCE_ID '${raw}'.`); } return raw; } function resolveDefaultConfigPath(): string { return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId(), "config.json"); } function readConfig(configPath: string): PartialConfig | null { if (!existsSync(configPath)) return null; try { const parsed = JSON.parse(readFileSync(configPath, "utf8")); return typeof parsed === "object" && parsed ? (parsed as PartialConfig) : null; } catch { return null; } } function asPositiveInt(value: unknown): number | null { if (typeof value !== "number" || !Number.isFinite(value)) return null; const rounded = Math.trunc(value); return rounded > 0 ? rounded : null; } function resolveEmbeddedPort(config: PartialConfig | null): number { return asPositiveInt(config?.database?.embeddedPostgresPort) ?? 54329; } function resolveConnectionString(config: PartialConfig | null): string { const envUrl = process.env.DATABASE_URL?.trim(); if (envUrl) return envUrl; if (config?.database?.mode === "postgres" && typeof config.database.connectionString === "string") { const trimmed = config.database.connectionString.trim(); if (trimmed) return trimmed; } const port = resolveEmbeddedPort(config); return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`; } function resolveDefaultBackupDir(): string { return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId(), "data", "backups"); } function resolveBackupDir(config: PartialConfig | null): string { const raw = config?.database?.backup?.dir; if (typeof raw === "string" && raw.trim().length > 0) { return path.resolve(expandHomePrefix(raw.trim())); } return resolveDefaultBackupDir(); } function resolveRetentionDays(config: PartialConfig | null): number { return asPositiveInt(config?.database?.backup?.retentionDays) ?? 30; } async function main() { const configPath = resolveDefaultConfigPath(); const config = readConfig(configPath); const connectionString = resolveConnectionString(config); const backupDir = resolveBackupDir(config); const retentionDays = resolveRetentionDays(config); console.log(`Config path: ${configPath}`); console.log(`Backing up database to: ${backupDir}`); console.log(`Retention window: ${retentionDays} day(s)`); try { const result = await runDatabaseBackup({ connectionString, backupDir, retentionDays, filenamePrefix: "paperclip", }); console.log(`Backup saved: ${formatDatabaseBackupResult(result)}`); } catch (err) { console.error("Backup failed."); if (err instanceof Error) { console.error(err.message); } else { console.error(String(err)); } process.exit(1); } } await main();