1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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)
}
|