From 69448e29a85cad3a94b3be3ad33efbc52764528f Mon Sep 17 00:00:00 2001 From: ivar Date: Mon, 9 Mar 2026 23:05:38 +0100 Subject: Add wip cli --- cli/src/commands/backup.ts | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 cli/src/commands/backup.ts (limited to 'cli/src/commands/backup.ts') diff --git a/cli/src/commands/backup.ts b/cli/src/commands/backup.ts new file mode 100644 index 0000000..4a7e4f0 --- /dev/null +++ b/cli/src/commands/backup.ts @@ -0,0 +1,75 @@ +import { mkdirSync, readdirSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import * as p from "@clack/prompts" +import * as actualApi from "@actual-app/api" +import { CONFIG_DIR, loadConfig } from "../config" +import { initActual } from "../actual" + +const BACKUP_DIR = join(CONFIG_DIR, "backups") + +export async function backup() { + const config = loadConfig() + + const spinner = p.spinner() + spinner.start("Connecting to Actual...") + await initActual(config.actual) + spinner.stop("Connected.") + + spinner.start("Exporting budget...") + const result = await (actualApi as any).internal.send("export-budget") + if (result.error) throw new Error(`Export failed: ${result.error}`) + + mkdirSync(BACKUP_DIR, { recursive: true }) + const filename = `backup-${timestamp()}.zip` + const filepath = join(BACKUP_DIR, filename) + writeFileSync(filepath, result.data) + + spinner.stop(`Backup saved to ${filepath}`) +} + +export async function restore(args: string[]) { + const config = loadConfig() + + const backups = listBackups() + if (!backups.length) { + p.log.warn(`No backups found in ${BACKUP_DIR}`) + return + } + + const filepath = await p.select({ + message: "Select backup to restore", + options: backups.map(f => ({ value: join(BACKUP_DIR, f), label: f })) + }) + if (p.isCancel(filepath)) { p.cancel("Cancelled."); process.exit(0) } + + const confirmed = await p.confirm({ + message: `Restore ${filepath}? This will overwrite your current budget.` + }) + if (p.isCancel(confirmed) || !confirmed) { p.cancel("Cancelled."); process.exit(0) } + + const spinner = p.spinner() + spinner.start("Connecting to Actual...") + await initActual(config.actual) + + spinner.start("Restoring backup...") + const result = await (actualApi as any).internal.send("import-budget", { filepath, type: "actual" }) + if (result?.error) throw new Error(`Restore failed: ${result.error}`) + + await actualApi.sync() + spinner.stop("Restored and synced.") +} + +function listBackups(): string[] { + try { + return readdirSync(BACKUP_DIR) + .filter(f => f.endsWith(".zip")) + .sort() + .reverse() + } catch { + return [] + } +} + +function timestamp(): string { + return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19) +} -- cgit v1.3