mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-07 04:18:51 -05:00
* [AI] Cache the CLI's local budget between invocations
Every `actual <cmd>` call currently delta-syncs the budget with the
sync server via `api.downloadBudget`, which hits the server's
500-req/min rate limit on scripted workflows. Actual is local-first:
once the budget is on disk, most read commands do not need fresh
server data.
Introduce a CLI-only cache layer inside `withConnection` that
decides per invocation whether to skip, sync, or re-download:
- Cache state lives at `{dataDir}/.actual-cli/{syncId}/state.json`,
keyed by `syncId` to avoid the chicken-and-egg of not knowing the
on-disk `budgetId` before the first download. The on-disk id is
resolved via `api.getBudgets()` and persisted after first download.
- Read commands (list, balance, query run, …) skip the `/sync`
call while `now - lastSyncedAt < cacheTtl`. Write commands
(create, update, delete, set-*, etc.) sync before and after the
operation to keep server state consistent.
- Encrypted budgets force a sync per call since `api/load-budget`
does not re-verify the password.
- New `proper-lockfile`-backed shared/exclusive lock serializes
writes while allowing parallel reads. Reader markers live in
`{meta}/readers/`; writers sweep stale markers by PID.
New `actual sync` command with three modes: default (sync now),
`--status` (print cache age, TTL, stale flag), `--clear` (delete
cache, holding the exclusive lock to avoid racing writers).
New config surface, following the existing flag → env → config file
→ default precedence chain:
- `--cache-ttl <s>` / `ACTUAL_CACHE_TTL` / `cacheTtl` (default 60)
- `--refresh` / `--no-cache`
- `--lock-timeout <s>` / `ACTUAL_LOCK_TIMEOUT` / `lockTimeout` (10)
- `--no-lock` / `ACTUAL_NO_LOCK` / `noLock`
Every `withConnection` call site now passes an explicit
`{ mutates: boolean, skipBudget?: boolean }` so read/write intent is
visible at the edge.
The old `budgets sync` subcommand is removed — it silently diverged
from the new top-level `actual sync`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Simplify CLI cache/lock internals
Use a discriminated SyncDecision union so connection.ts no longer needs
non-null assertions on the cached state. Thread the resolved CliConfig
through withConnection's callback to drop duplicate resolveConfig calls
in the sync and budgets commands. Extract an errorCode helper and
replace the existsSync+readdirSync TOCTOU pattern in the reader-wait
polling loop with a single readdir that tolerates ENOENT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Address PR #7539 review comments
- cache.ts: use a unique per-writer tmp filename so concurrent shared-
lock writers (encrypted budgets, --refresh, stale TTL) don't clobber
each other's publish and silently drop state updates.
- index.ts/config.ts: fix --no-cache and --no-lock flags. Commander
stores --no-foo under the positive key (cache/lock) and an explicit
false default makes the flag a no-op; the previous code also read
the wrong keys (noCache/noLock) so the flags had no effect at all.
Derive refresh/noLock from the correct keys.
- budgets.ts: invert encryption password precedence so the subcommand
flag (--encryption-password) wins over env/config-file values.
- sync.ts: report stale=true in --status when lastSyncedAt is in the
future, matching decideSyncAction's clock-skew handling.
- connection.ts: drop unnecessary `as` cast on api.getBudgets() now
that the return type is Promise<APIFileEntity[]>.
- utils.ts: parseBoolEnv throws on unrecognized values instead of
silently returning undefined so typos like ACTUAL_NO_LOCK=yes fail
loudly.
- Shorten 7539 release note to a single user-facing sentence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
1.2 KiB
JSON
53 lines
1.2 KiB
JSON
{
|
|
"name": "@actual-app/cli",
|
|
"version": "26.4.0",
|
|
"description": "CLI for Actual Budget",
|
|
"license": "MIT",
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git+https://github.com/actualbudget/actual.git",
|
|
"directory": "packages/cli"
|
|
},
|
|
"bin": {
|
|
"actual": "./dist/cli.js",
|
|
"actual-cli": "./dist/cli.js"
|
|
},
|
|
"files": [
|
|
"dist"
|
|
],
|
|
"type": "module",
|
|
"imports": {
|
|
"#cache": "./src/cache.ts",
|
|
"#commands/*": "./src/commands/*.ts",
|
|
"#config": "./src/config.ts",
|
|
"#connection": "./src/connection.ts",
|
|
"#input": "./src/input.ts",
|
|
"#lock": "./src/lock.ts",
|
|
"#output": "./src/output.ts",
|
|
"#utils": "./src/utils.ts"
|
|
},
|
|
"scripts": {
|
|
"build": "vite build",
|
|
"test": "vitest --run",
|
|
"typecheck": "tsgo -b"
|
|
},
|
|
"dependencies": {
|
|
"@actual-app/api": "workspace:*",
|
|
"cli-table3": "^0.6.5",
|
|
"commander": "^14.0.3",
|
|
"cosmiconfig": "^9.0.1",
|
|
"proper-lockfile": "^4.1.2"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^22.19.17",
|
|
"@types/proper-lockfile": "^4",
|
|
"@typescript/native-preview": "beta",
|
|
"rollup-plugin-visualizer": "^7.0.1",
|
|
"vite": "^8.0.5",
|
|
"vitest": "^4.1.2"
|
|
},
|
|
"engines": {
|
|
"node": ">=22"
|
|
}
|
|
}
|