import path from "node:path"; import * as p from "@clack/prompts"; import pc from "picocolors"; import { formatDatabaseBackupResult, runDatabaseBackup } from "@paperclipai/db"; import { expandHomePrefix, resolveDefaultBackupDir, resolvePaperclipInstanceId, } from "../config/home.js"; import { readConfig, resolveConfigPath } from "../config/store.js"; import { printPaperclipCliBanner } from "../utils/banner.js"; type DbBackupOptions = { config?: string; dir?: string; retentionDays?: number; filenamePrefix?: string; json?: boolean; }; function resolveConnectionString(configPath?: string): { value: string; source: string } { const envUrl = process.env.DATABASE_URL?.trim(); if (envUrl) return { value: envUrl, source: "DATABASE_URL" }; const config = readConfig(configPath); if (config?.database.mode === "postgres" && config.database.connectionString?.trim()) { return { value: config.database.connectionString.trim(), source: "config.database.connectionString" }; } const port = config?.database.embeddedPostgresPort ?? 54329; return { value: `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`, source: `embedded-postgres@${port}`, }; } function normalizeRetentionDays(value: number | undefined, fallback: number): number { const candidate = value ?? fallback; if (!Number.isInteger(candidate) || candidate < 1) { throw new Error(`Invalid retention days '${String(candidate)}'. Use a positive integer.`); } return candidate; } function resolveBackupDir(raw: string): string { return path.resolve(expandHomePrefix(raw.trim())); } export async function dbBackupCommand(opts: DbBackupOptions): Promise { printPaperclipCliBanner(); p.intro(pc.bgCyan(pc.black(" paperclip db:backup "))); const configPath = resolveConfigPath(opts.config); const config = readConfig(opts.config); const connection = resolveConnectionString(opts.config); const defaultDir = resolveDefaultBackupDir(resolvePaperclipInstanceId()); const configuredDir = opts.dir?.trim() || config?.database.backup.dir || defaultDir; const backupDir = resolveBackupDir(configuredDir); const retentionDays = normalizeRetentionDays( opts.retentionDays, config?.database.backup.retentionDays ?? 30, ); const filenamePrefix = opts.filenamePrefix?.trim() || "paperclip"; p.log.message(pc.dim(`Config: ${configPath}`)); p.log.message(pc.dim(`Connection source: ${connection.source}`)); p.log.message(pc.dim(`Backup dir: ${backupDir}`)); p.log.message(pc.dim(`Retention: ${retentionDays} day(s)`)); const spinner = p.spinner(); spinner.start("Creating database backup..."); try { const result = await runDatabaseBackup({ connectionString: connection.value, backupDir, retentionDays, filenamePrefix, }); spinner.stop(`Backup saved: ${formatDatabaseBackupResult(result)}`); if (opts.json) { console.log( JSON.stringify( { backupFile: result.backupFile, sizeBytes: result.sizeBytes, prunedCount: result.prunedCount, backupDir, retentionDays, connectionSource: connection.source, }, null, 2, ), ); } p.outro(pc.green("Backup completed.")); } catch (err) { spinner.stop(pc.red("Backup failed.")); throw err; } }