feat(KB-327): add automatic database backup system
- Add backup settings to ProjectSettings with schedule, retention, and directory config - Create BackupManager with create, list, cleanup, and restore operations - Add CLI backup commands: --create, --list, --restore, --cleanup - Implement backup validation and automation sync for scheduled backups - Add dashboard backup settings UI with stats and 'Backup Now' button - Add backup API routes for settings management and manual operations - Include comprehensive backup tests and changeset for patch release - Document backup configuration and recovery in AGENTS.md
This commit is contained in:
@@ -42,6 +42,7 @@ const { runDashboard } = await import("./commands/dashboard.js");
|
||||
const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskLogs, runTaskShow, runTaskAttach, runTaskPause, runTaskUnpause, runTaskImportFromGitHub, runTaskDuplicate, runTaskArchive, runTaskUnarchive, runTaskRefine, runTaskPlan, runTaskDelete, runTaskRetry, runTaskComment, runTaskComments, runTaskSteer, runTaskPrCreate } = await import("./commands/task.js");
|
||||
const { runSettingsShow, runSettingsSet } = await import("./commands/settings.js");
|
||||
const { runGitStatus, runGitFetch, runGitPull, runGitPush } = await import("./commands/git.js");
|
||||
const { runBackupCreate, runBackupList, runBackupRestore, runBackupCleanup } = await import("./commands/backup.js");
|
||||
|
||||
const HELP = `
|
||||
fn — AI-orchestrated task board
|
||||
@@ -83,6 +84,10 @@ Usage:
|
||||
fn git push Push current branch
|
||||
fn git pull Pull current branch
|
||||
fn git fetch [remote] Fetch from remote (default: origin)
|
||||
fn backup --create Create a database backup immediately
|
||||
fn backup --list List all database backups
|
||||
fn backup --restore <file> Restore database from a backup file
|
||||
fn backup --cleanup Remove old backups exceeding retention limit
|
||||
|
||||
Options:
|
||||
--port, -p <port> Dashboard port (default: 4040)
|
||||
@@ -463,6 +468,28 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
|
||||
case "backup": {
|
||||
const create = args.includes("--create");
|
||||
const list = args.includes("--list");
|
||||
const cleanup = args.includes("--cleanup");
|
||||
const restoreIdx = args.indexOf("--restore");
|
||||
const restoreFile = restoreIdx !== -1 && restoreIdx + 1 < args.length ? args[restoreIdx + 1] : undefined;
|
||||
|
||||
if (create) {
|
||||
await runBackupCreate();
|
||||
} else if (list) {
|
||||
await runBackupList();
|
||||
} else if (cleanup) {
|
||||
await runBackupCleanup();
|
||||
} else if (restoreFile) {
|
||||
await runBackupRestore(restoreFile);
|
||||
} else {
|
||||
console.error("Usage: fn backup --create | --list | --cleanup | --restore <filename>");
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.log(HELP);
|
||||
|
||||
126
packages/cli/src/commands/backup.ts
Normal file
126
packages/cli/src/commands/backup.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
BackupManager,
|
||||
createBackupManager,
|
||||
runBackupCommand,
|
||||
TaskStore,
|
||||
} from "@fusion/core";
|
||||
|
||||
/**
|
||||
* Find the project root and create a backup manager.
|
||||
*/
|
||||
async function getBackupManager(): Promise<{
|
||||
manager: BackupManager;
|
||||
store: TaskStore;
|
||||
kbDir: string;
|
||||
}> {
|
||||
const store = new TaskStore(process.cwd());
|
||||
await store.init();
|
||||
// Access the private kbDir property via type assertion
|
||||
const kbDir = (store as unknown as { kbDir: string }).kbDir;
|
||||
const settings = await store.getSettings();
|
||||
const manager = createBackupManager(kbDir, settings);
|
||||
return { manager, store, kbDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a database backup immediately.
|
||||
* Usage: kb backup --create
|
||||
*/
|
||||
export async function runBackupCreate(): Promise<void> {
|
||||
const { manager, kbDir, store } = await getBackupManager();
|
||||
const settings = await store.getSettings();
|
||||
|
||||
console.log("Creating database backup...");
|
||||
|
||||
const result = await runBackupCommand(kbDir, settings);
|
||||
|
||||
if (result.success) {
|
||||
console.log(result.output);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(result.output);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all database backups.
|
||||
* Usage: kb backup --list
|
||||
*/
|
||||
export async function runBackupList(): Promise<void> {
|
||||
const { manager } = await getBackupManager();
|
||||
|
||||
const backups = await manager.listBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log("No backups found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${backups.length} backup(s):\n`);
|
||||
|
||||
// Calculate total size
|
||||
const totalSize = backups.reduce((sum, b) => sum + b.size, 0);
|
||||
const formattedTotal = formatBytes(totalSize);
|
||||
|
||||
console.log("Date Size Filename");
|
||||
console.log("-".repeat(60));
|
||||
|
||||
for (const backup of backups) {
|
||||
const date = new Date(backup.createdAt).toLocaleString();
|
||||
const size = formatBytes(backup.size).padEnd(8);
|
||||
console.log(`${date} ${size} ${backup.filename}`);
|
||||
}
|
||||
|
||||
console.log("-".repeat(60));
|
||||
console.log(`Total: ${formattedTotal}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore database from a backup file.
|
||||
* Usage: kb backup --restore <filename>
|
||||
*/
|
||||
export async function runBackupRestore(filename: string): Promise<void> {
|
||||
const { manager } = await getBackupManager();
|
||||
|
||||
console.log(`Restoring backup: ${filename}`);
|
||||
console.log("A pre-restore backup will be created first.\n");
|
||||
|
||||
try {
|
||||
await manager.restoreBackup(filename, { createPreRestoreBackup: true });
|
||||
console.log(`Successfully restored from ${filename}`);
|
||||
console.log("Note: The pre-restore backup was saved in case you need to undo this operation.");
|
||||
} catch (err) {
|
||||
console.error(`Restore failed: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old backups exceeding retention limit.
|
||||
* Usage: kb backup --cleanup
|
||||
*/
|
||||
export async function runBackupCleanup(): Promise<void> {
|
||||
const { manager } = await getBackupManager();
|
||||
|
||||
console.log("Cleaning up old backups...");
|
||||
|
||||
const deletedCount = await manager.cleanupOldBackups();
|
||||
|
||||
if (deletedCount > 0) {
|
||||
console.log(`Removed ${deletedCount} old backup(s).`);
|
||||
} else {
|
||||
console.log("No backups to clean up (within retention limit).");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes as human-readable string.
|
||||
*/
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user