Add paperclipai db:backup CLI command
This commit is contained in:
102
cli/src/commands/db-backup.ts
Normal file
102
cli/src/commands/db-backup.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { addAllowedHostname } from "./commands/allowed-hostname.js";
|
||||
import { heartbeatRun } from "./commands/heartbeat-run.js";
|
||||
import { runCommand } from "./commands/run.js";
|
||||
import { bootstrapCeoInvite } from "./commands/auth-bootstrap-ceo.js";
|
||||
import { dbBackupCommand } from "./commands/db-backup.js";
|
||||
import { registerContextCommands } from "./commands/client/context.js";
|
||||
import { registerCompanyCommands } from "./commands/client/company.js";
|
||||
import { registerIssueCommands } from "./commands/client/issue.js";
|
||||
@@ -70,6 +71,19 @@ program
|
||||
.option("-s, --section <section>", "Section to configure (llm, database, logging, server, storage, secrets)")
|
||||
.action(configure);
|
||||
|
||||
program
|
||||
.command("db:backup")
|
||||
.description("Create a one-off database backup using current config")
|
||||
.option("-c, --config <path>", "Path to config file")
|
||||
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
|
||||
.option("--dir <path>", "Backup output directory (overrides config)")
|
||||
.option("--retention-days <days>", "Retention window used for pruning", (value) => Number(value))
|
||||
.option("--filename-prefix <prefix>", "Backup filename prefix", "paperclip")
|
||||
.option("--json", "Print backup metadata as JSON")
|
||||
.action(async (opts) => {
|
||||
await dbBackupCommand(opts);
|
||||
});
|
||||
|
||||
program
|
||||
.command("allowed-hostname")
|
||||
.description("Allow a hostname for authenticated/private mode access")
|
||||
|
||||
@@ -156,6 +156,14 @@ Configure these in:
|
||||
pnpm paperclipai configure --section database
|
||||
```
|
||||
|
||||
Run a one-off backup manually:
|
||||
|
||||
```sh
|
||||
pnpm paperclipai db:backup
|
||||
# or:
|
||||
pnpm db:backup
|
||||
```
|
||||
|
||||
Environment overrides:
|
||||
|
||||
- `PAPERCLIP_DB_BACKUP_ENABLED=true|false`
|
||||
|
||||
@@ -14,4 +14,4 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
exec pnpm --filter @paperclipai/db exec tsx src/backup.ts "$@"
|
||||
exec pnpm paperclipai db:backup "$@"
|
||||
|
||||
Reference in New Issue
Block a user