feat(FN-3891): document memory backup settings in settings reference
Documents the memory backup feature in the settings reference and adds a changeset to prepare for publishing. Fusion-Task-Id: FN-3891
This commit is contained in:
@@ -124,6 +124,7 @@ async function loadCommandHandlers() {
|
||||
const { runSettingsImport } = await import("./commands/settings-import.js");
|
||||
const { runGitStatus, runGitFetch, runGitPull, runGitPush } = await import("./commands/git.js");
|
||||
const { runBackupCreate, runBackupList, runBackupRestore, runBackupCleanup } = await import("./commands/backup.js");
|
||||
const { runMemoryBackupCreate, runMemoryBackupList, runMemoryBackupRestore } = await import("./commands/memory-backup.js");
|
||||
const { runMissionCreate, runMissionList, runMissionShow, runMissionDelete, runMissionActivateSlice } = await import("./commands/mission.js");
|
||||
const { runProjectList, runProjectAdd, runProjectRemove, runProjectShow, runProjectInfo, runProjectSetDefault, runProjectDetect } = await import("./commands/project.js");
|
||||
const { runNodeList, runNodeConnect, runNodeDisconnect, runNodeShow, runNodeHealth, runMeshStatus } = await import("./commands/node.js");
|
||||
@@ -180,6 +181,9 @@ async function loadCommandHandlers() {
|
||||
runBackupList,
|
||||
runBackupRestore,
|
||||
runBackupCleanup,
|
||||
runMemoryBackupCreate,
|
||||
runMemoryBackupList,
|
||||
runMemoryBackupRestore,
|
||||
runMissionCreate,
|
||||
runMissionList,
|
||||
runMissionShow,
|
||||
@@ -339,6 +343,11 @@ Usage:
|
||||
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
|
||||
fn memory-backup --create [--scope <project|agents|all>]
|
||||
Create a memory backup immediately
|
||||
fn memory-backup --list List all memory backups
|
||||
fn memory-backup --restore <dir>
|
||||
Restore memory from a backup directory snapshot
|
||||
fn plugin list | ls List installed plugins
|
||||
fn plugin install <path-or-package> [--ai-scan] Install a plugin from path or package
|
||||
fn plugin add <path-or-package> Alias for plugin install
|
||||
@@ -531,6 +540,9 @@ async function main() {
|
||||
runBackupList,
|
||||
runBackupRestore,
|
||||
runBackupCleanup,
|
||||
runMemoryBackupCreate,
|
||||
runMemoryBackupList,
|
||||
runMemoryBackupRestore,
|
||||
runMissionCreate,
|
||||
runMissionList,
|
||||
runMissionShow,
|
||||
@@ -1332,6 +1344,31 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
|
||||
case "memory-backup": {
|
||||
const create = args.includes("--create");
|
||||
const list = args.includes("--list");
|
||||
const restoreIdx = args.indexOf("--restore");
|
||||
const restoreFile = restoreIdx !== -1 && restoreIdx + 1 < args.length ? args[restoreIdx + 1] : undefined;
|
||||
const scopeIdx = args.indexOf("--scope");
|
||||
const scope = scopeIdx !== -1 && scopeIdx + 1 < args.length ? args[scopeIdx + 1] : undefined;
|
||||
|
||||
if (create) {
|
||||
if (scope && !["project", "agents", "all"].includes(scope)) {
|
||||
console.error("Usage: fn memory-backup --create [--scope <project|agents|all>]");
|
||||
process.exit(1);
|
||||
}
|
||||
await runMemoryBackupCreate({ projectName, scope: scope as "project" | "agents" | "all" | undefined });
|
||||
} else if (list) {
|
||||
await runMemoryBackupList(projectName);
|
||||
} else if (restoreFile) {
|
||||
await runMemoryBackupRestore(restoreFile, projectName);
|
||||
} else {
|
||||
console.error("Usage: fn memory-backup --create [--scope <project|agents|all>] | --list | --restore <filename>");
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "agent": {
|
||||
const subcommand = args[1];
|
||||
switch (subcommand) {
|
||||
|
||||
90
packages/cli/src/commands/__tests__/memory-backup.test.ts
Normal file
90
packages/cli/src/commands/__tests__/memory-backup.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const {
|
||||
mockListBackups,
|
||||
mockRestoreBackup,
|
||||
mockGetSettings,
|
||||
mockRunMemoryBackupCommand,
|
||||
mockResolveProject,
|
||||
} = vi.hoisted(() => ({
|
||||
mockListBackups: vi.fn(),
|
||||
mockRestoreBackup: vi.fn(),
|
||||
mockGetSettings: vi.fn(),
|
||||
mockRunMemoryBackupCommand: vi.fn(),
|
||||
mockResolveProject: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
TaskStore: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
getSettings: mockGetSettings,
|
||||
fusionDir: "/cwd/.fusion",
|
||||
})),
|
||||
createMemoryBackupManager: vi.fn(() => ({
|
||||
listBackups: mockListBackups,
|
||||
restoreBackup: mockRestoreBackup,
|
||||
})),
|
||||
runMemoryBackupCommand: mockRunMemoryBackupCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../project-context.js", () => ({
|
||||
resolveProject: mockResolveProject,
|
||||
}));
|
||||
|
||||
import { runMemoryBackupCreate, runMemoryBackupList, runMemoryBackupRestore } from "../memory-backup.js";
|
||||
|
||||
describe("memory-backup commands", () => {
|
||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||
let errorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let exitSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
});
|
||||
|
||||
mockGetSettings.mockResolvedValue({ memoryBackupSchedule: "0 3 * * *" });
|
||||
mockRunMemoryBackupCommand.mockResolvedValue({ success: true, output: "memory backup created" });
|
||||
mockListBackups.mockResolvedValue([]);
|
||||
mockRestoreBackup.mockResolvedValue(undefined);
|
||||
mockResolveProject.mockResolvedValue({
|
||||
store: { getSettings: mockGetSettings, fusionDir: "/projects/demo/.fusion" },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("create path succeeds", async () => {
|
||||
await expect(runMemoryBackupCreate({ projectName: "demo", scope: "agents" })).rejects.toThrow("process.exit:0");
|
||||
expect(mockRunMemoryBackupCommand).toHaveBeenCalledWith(
|
||||
"/projects/demo/.fusion",
|
||||
expect.objectContaining({ memoryBackupScope: "agents" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("list path renders entries", async () => {
|
||||
mockListBackups.mockResolvedValue([
|
||||
{ filename: "memory-2026-01-01-000000", createdAt: new Date().toISOString(), size: 100, scope: "all", entryCount: 3 },
|
||||
]);
|
||||
await runMemoryBackupList("demo");
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Found 1 memory backup"));
|
||||
});
|
||||
|
||||
it("restore path calls manager", async () => {
|
||||
await runMemoryBackupRestore("memory-2026-01-01-000000", "demo");
|
||||
expect(mockRestoreBackup).toHaveBeenCalledWith("memory-2026-01-01-000000", { overwrite: true });
|
||||
});
|
||||
|
||||
it("create path fails on invalid schedule", async () => {
|
||||
mockRunMemoryBackupCommand.mockResolvedValue({ success: false, output: "Invalid memory backup schedule: bad" });
|
||||
await expect(runMemoryBackupCreate({ projectName: "demo" })).rejects.toThrow("process.exit:1");
|
||||
expect(errorSpy).toHaveBeenCalledWith("Invalid memory backup schedule: bad");
|
||||
});
|
||||
});
|
||||
96
packages/cli/src/commands/memory-backup.ts
Normal file
96
packages/cli/src/commands/memory-backup.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
createMemoryBackupManager,
|
||||
runMemoryBackupCommand,
|
||||
TaskStore,
|
||||
type ProjectSettings,
|
||||
} from "@fusion/core";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
type MemoryBackupScope = "project" | "agents" | "all";
|
||||
|
||||
async function resolveBackupStore(projectName?: string): Promise<TaskStore> {
|
||||
try {
|
||||
return (await resolveProject(projectName)).store;
|
||||
} catch {
|
||||
const store = new TaskStore(process.cwd());
|
||||
await store.init();
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
async function getMemoryBackupContext(projectName?: string): Promise<{
|
||||
store: TaskStore;
|
||||
fusionDir: string;
|
||||
settings: ProjectSettings;
|
||||
}> {
|
||||
const store = await resolveBackupStore(projectName);
|
||||
const fusionDir = (store as unknown as { fusionDir: string }).fusionDir;
|
||||
const settings = await store.getSettings();
|
||||
return { store, fusionDir, settings };
|
||||
}
|
||||
|
||||
export async function runMemoryBackupCreate(options?: { projectName?: string; scope?: MemoryBackupScope }): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(options?.projectName);
|
||||
const effectiveSettings = options?.scope ? { ...settings, memoryBackupScope: options.scope } : settings;
|
||||
|
||||
console.log("Creating memory backup...");
|
||||
const result = await runMemoryBackupCommand(fusionDir, effectiveSettings);
|
||||
if (result.success) {
|
||||
console.log(result.output);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(result.output);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export async function runMemoryBackupList(projectName?: string): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(projectName);
|
||||
const manager = createMemoryBackupManager(fusionDir, settings);
|
||||
const backups = await manager.listBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log("No memory backups found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${backups.length} memory backup(s):\n`);
|
||||
console.log("Date Scope Entries Size Filename");
|
||||
console.log("-".repeat(80));
|
||||
|
||||
let totalSize = 0;
|
||||
for (const backup of backups) {
|
||||
totalSize += backup.size;
|
||||
const date = new Date(backup.createdAt).toLocaleString();
|
||||
const scope = backup.scope.padEnd(7);
|
||||
const entries = String(backup.entryCount).padEnd(7);
|
||||
const size = formatBytes(backup.size).padEnd(9);
|
||||
console.log(`${date} ${scope} ${entries} ${size} ${backup.filename}`);
|
||||
}
|
||||
|
||||
console.log("-".repeat(80));
|
||||
console.log(`Total: ${formatBytes(totalSize)}`);
|
||||
}
|
||||
|
||||
export async function runMemoryBackupRestore(filename: string, projectName?: string): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(projectName);
|
||||
const manager = createMemoryBackupManager(fusionDir, settings);
|
||||
|
||||
console.log(`Restoring memory backup: ${filename}`);
|
||||
console.log("This may overwrite project and/or agent memory files.\n");
|
||||
|
||||
try {
|
||||
await manager.restoreBackup(filename, { overwrite: true });
|
||||
console.log(`Successfully restored memory from ${filename}`);
|
||||
} catch (err) {
|
||||
console.error(`Memory restore failed: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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