mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-14 22:15:36 -05:00
Compare commits
5 Commits
master
...
claude/act
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62a68bd323 | ||
|
|
7f6348cd83 | ||
|
|
8899b13548 | ||
|
|
8de530daec | ||
|
|
d8f2d822b0 |
202
.claude/skills/actual/SKILL.md
Normal file
202
.claude/skills/actual/SKILL.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: actual
|
||||
description: Interact with Actual Budget - query and modify your budget data via the CLI tool
|
||||
allowed-tools: Bash(node packages/cli/dist/src/index.js *)
|
||||
disable-model-invocation: true
|
||||
argument-hint: <natural language request about your budget>
|
||||
---
|
||||
|
||||
# Actual Budget CLI Skill
|
||||
|
||||
You are an assistant that helps users interact with their Actual Budget data using the `@actual-app/cli` tool.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following environment variables must be set:
|
||||
|
||||
- `ACTUAL_SERVER_URL` - URL of the Actual sync server (required)
|
||||
- `ACTUAL_BUDGET_ID` - Budget ID to load (required for most operations)
|
||||
- `ACTUAL_PASSWORD` or `ACTUAL_SESSION_TOKEN` - Authentication (one required)
|
||||
|
||||
If these are not set, ask the user to configure them before proceeding.
|
||||
|
||||
## CLI Command Reference
|
||||
|
||||
Run commands using: `node packages/cli/dist/src/index.js <command> [options]`
|
||||
|
||||
Add `--quiet` to suppress informational messages when piping output.
|
||||
|
||||
### Global Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------- | ----------------------------------------------- |
|
||||
| `--server-url <url>` | Server URL (overrides env) |
|
||||
| `--password <pw>` | Password (overrides env) |
|
||||
| `--session-token <token>` | Session token (overrides env) |
|
||||
| `--budget-id <id>` | Budget ID (overrides env) |
|
||||
| `--data-dir <path>` | Data directory |
|
||||
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
|
||||
| `--quiet` | Suppress stderr info messages |
|
||||
|
||||
### Accounts
|
||||
|
||||
```bash
|
||||
actual accounts list
|
||||
actual accounts create --name "Checking" [--offbudget] [--balance 50000]
|
||||
actual accounts update <id> [--name "New Name"] [--offbudget true]
|
||||
actual accounts close <id> [--transfer-account <id>] [--transfer-category <id>]
|
||||
actual accounts reopen <id>
|
||||
actual accounts delete <id>
|
||||
actual accounts balance <id> [--cutoff 2026-01-31]
|
||||
```
|
||||
|
||||
### Budgets
|
||||
|
||||
```bash
|
||||
actual budgets list
|
||||
actual budgets download <syncId> [--encryption-password <pw>]
|
||||
actual budgets sync
|
||||
actual budgets months
|
||||
actual budgets month 2026-03
|
||||
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
|
||||
actual budgets set-carryover --month 2026-03 --category <id> --flag true
|
||||
actual budgets hold-next-month --month 2026-03 --amount 10000
|
||||
actual budgets reset-hold --month 2026-03
|
||||
```
|
||||
|
||||
### Categories
|
||||
|
||||
```bash
|
||||
actual categories list
|
||||
actual categories create --name "Groceries" --group-id <id> [--is-income]
|
||||
actual categories update <id> [--name "Food"] [--hidden true]
|
||||
actual categories delete <id> [--transfer-to <id>]
|
||||
```
|
||||
|
||||
### Category Groups
|
||||
|
||||
```bash
|
||||
actual category-groups list
|
||||
actual category-groups create --name "Essentials" [--is-income]
|
||||
actual category-groups update <id> [--name "New Name"] [--hidden true]
|
||||
actual category-groups delete <id> [--transfer-to <id>]
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```bash
|
||||
actual transactions list --account <id> --start 2026-01-01 --end 2026-03-31
|
||||
actual transactions add --account <id> --data '[{"date":"2026-03-13","amount":-5000,"payee_name":"Store"}]'
|
||||
actual transactions add --account <id> --file transactions.json
|
||||
actual transactions import --account <id> --data '[...]' [--dry-run]
|
||||
actual transactions update <id> --data '{"notes":"Updated note"}'
|
||||
actual transactions delete <id>
|
||||
```
|
||||
|
||||
### Payees
|
||||
|
||||
```bash
|
||||
actual payees list
|
||||
actual payees common
|
||||
actual payees create --name "Grocery Store"
|
||||
actual payees update <id> --name "New Name"
|
||||
actual payees delete <id>
|
||||
actual payees merge --target <id> --ids id1,id2,id3
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
```bash
|
||||
actual tags list
|
||||
actual tags create --tag "vacation" [--color "#ff0000"] [--description "Vacation expenses"]
|
||||
actual tags update <id> [--tag "trip"] [--color "#00ff00"]
|
||||
actual tags delete <id>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
```bash
|
||||
actual rules list
|
||||
actual rules payee-rules <payeeId>
|
||||
actual rules create --data '{"stage":"pre","conditionsOp":"and","conditions":[...],"actions":[...]}'
|
||||
actual rules create --file rule.json
|
||||
actual rules update --data '{"id":"...","stage":"pre",...}'
|
||||
actual rules delete <id>
|
||||
```
|
||||
|
||||
### Schedules
|
||||
|
||||
```bash
|
||||
actual schedules list
|
||||
actual schedules create --data '{"name":"Rent","date":"1st","amount":-150000,"amountOp":"is","account":"...","payee":"..."}'
|
||||
actual schedules update <id> --data '{"name":"Updated Rent"}' [--reset-next-date]
|
||||
actual schedules delete <id>
|
||||
```
|
||||
|
||||
### Query (AQL)
|
||||
|
||||
```bash
|
||||
actual query run --table transactions --select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
|
||||
actual query run --file query.json
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
```bash
|
||||
actual server version
|
||||
actual server get-id --type accounts --name "Checking"
|
||||
actual server get-id --type categories --name "Groceries"
|
||||
actual server bank-sync [--account <id>]
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Amount Convention
|
||||
|
||||
All monetary amounts are in **integer cents**. When displaying to users:
|
||||
|
||||
- `5000` = $50.00
|
||||
- `-12350` = -$123.50
|
||||
- Always convert cents to dollars for user-facing display (divide by 100)
|
||||
- When the user says "$50", convert to `5000` for the CLI
|
||||
|
||||
### Workflow Tips
|
||||
|
||||
1. **Find entity IDs by name**: Use `server get-id --type <type> --name "<name>"` before operations that need IDs
|
||||
2. **Explore first**: Start with `list` commands to understand what data exists
|
||||
3. **Chain operations**: Use `--quiet --format json` when piping between commands
|
||||
4. **Complex objects**: For rules/schedules, create a JSON file and use `--file`
|
||||
|
||||
### Common Workflows
|
||||
|
||||
**"Show my budget for this month":**
|
||||
|
||||
```bash
|
||||
actual budgets month 2026-03 --quiet
|
||||
```
|
||||
|
||||
**"How much have I spent on groceries?":**
|
||||
|
||||
```bash
|
||||
# First get the category ID
|
||||
actual server get-id --type categories --name "Groceries" --quiet
|
||||
# Then query transactions
|
||||
actual transactions list --account <id> --start 2026-03-01 --end 2026-03-31 --quiet
|
||||
```
|
||||
|
||||
**"What's my checking account balance?":**
|
||||
|
||||
```bash
|
||||
actual server get-id --type accounts --name "Checking" --quiet
|
||||
actual accounts balance <id> --quiet
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Non-zero exit codes indicate errors
|
||||
- Error JSON is written to stdout: `{"error": "message"}`
|
||||
- Always check the output before presenting results to the user
|
||||
|
||||
## User Request
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -40,6 +40,7 @@
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"build:cli": "yarn workspace @actual-app/cli build",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"build:storybook": "yarn workspace @actual-app/components build:storybook",
|
||||
"deploy:docs": "yarn workspace docs deploy",
|
||||
|
||||
2
packages/cli/.gitignore
vendored
Normal file
2
packages/cli/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
coverage
|
||||
33
packages/cli/package.json
Normal file
33
packages/cli/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@actual-app/cli",
|
||||
"version": "26.3.0",
|
||||
"description": "CLI for Actual Budget",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"actual": "./dist/src/index.js",
|
||||
"actual-cli": "./dist/src/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/api": "workspace:^",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^13.0.0",
|
||||
"cosmiconfig": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.10",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
114
packages/cli/src/commands/accounts.ts
Normal file
114
packages/cli/src/commands/accounts.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerAccountsCommand(program: Command) {
|
||||
const accounts = program.command('accounts').description('Manage accounts');
|
||||
|
||||
accounts
|
||||
.command('list')
|
||||
.description('List all accounts')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getAccounts();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('create')
|
||||
.description('Create a new account')
|
||||
.requiredOption('--name <name>', 'Account name')
|
||||
.option('--offbudget', 'Create as off-budget account', false)
|
||||
.option('--balance <amount>', 'Initial balance in cents', '0')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.createAccount(
|
||||
{ name: cmdOpts.name, offbudget: cmdOpts.offbudget },
|
||||
parseInt(cmdOpts.balance, 10),
|
||||
);
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('update <id>')
|
||||
.description('Update an account')
|
||||
.option('--name <name>', 'New account name')
|
||||
.option('--offbudget <bool>', 'Set off-budget status')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (cmdOpts.name) fields.name = cmdOpts.name;
|
||||
if (cmdOpts.offbudget !== undefined) {
|
||||
fields.offbudget = cmdOpts.offbudget === 'true';
|
||||
}
|
||||
await api.updateAccount(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('close <id>')
|
||||
.description('Close an account')
|
||||
.option(
|
||||
'--transfer-account <id>',
|
||||
'Transfer remaining balance to this account',
|
||||
)
|
||||
.option(
|
||||
'--transfer-category <id>',
|
||||
'Transfer remaining balance to this category',
|
||||
)
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.closeAccount(
|
||||
id,
|
||||
cmdOpts.transferAccount,
|
||||
cmdOpts.transferCategory,
|
||||
);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('reopen <id>')
|
||||
.description('Reopen a closed account')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.reopenAccount(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('delete <id>')
|
||||
.description('Delete an account')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteAccount(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
accounts
|
||||
.command('balance <id>')
|
||||
.description('Get account balance')
|
||||
.option('--cutoff <date>', 'Cutoff date (YYYY-MM-DD)')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const cutoff = cmdOpts.cutoff ? new Date(cmdOpts.cutoff) : undefined;
|
||||
const balance = await api.getAccountBalance(id, cutoff);
|
||||
printOutput({ id, balance }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
132
packages/cli/src/commands/budgets.ts
Normal file
132
packages/cli/src/commands/budgets.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerBudgetsCommand(program: Command) {
|
||||
const budgets = program.command('budgets').description('Manage budgets');
|
||||
|
||||
budgets
|
||||
.command('list')
|
||||
.description('List all available budgets')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getBudgets();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('download <syncId>')
|
||||
.description('Download a budget by sync ID')
|
||||
.option('--encryption-password <password>', 'Encryption password')
|
||||
.action(async (syncId: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.downloadBudget(syncId, {
|
||||
password: cmdOpts.encryptionPassword,
|
||||
});
|
||||
printOutput({ success: true, syncId }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('sync')
|
||||
.description('Sync the current budget')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.sync();
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('months')
|
||||
.description('List available budget months')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getBudgetMonths();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('month <month>')
|
||||
.description('Get budget data for a specific month (YYYY-MM)')
|
||||
.action(async (month: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getBudgetMonth(month);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('set-amount')
|
||||
.description('Set budget amount for a category in a month')
|
||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
||||
.requiredOption('--category <id>', 'Category ID')
|
||||
.requiredOption('--amount <amount>', 'Amount in cents')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.setBudgetAmount(
|
||||
cmdOpts.month,
|
||||
cmdOpts.category,
|
||||
parseInt(cmdOpts.amount, 10),
|
||||
);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('set-carryover')
|
||||
.description('Enable/disable carryover for a category')
|
||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
||||
.requiredOption('--category <id>', 'Category ID')
|
||||
.requiredOption('--flag <bool>', 'Enable (true) or disable (false)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.setBudgetCarryover(
|
||||
cmdOpts.month,
|
||||
cmdOpts.category,
|
||||
cmdOpts.flag === 'true',
|
||||
);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('hold-next-month')
|
||||
.description('Hold budget amount for next month')
|
||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
||||
.requiredOption('--amount <amount>', 'Amount in cents')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.holdBudgetForNextMonth(
|
||||
cmdOpts.month,
|
||||
parseInt(cmdOpts.amount, 10),
|
||||
);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
budgets
|
||||
.command('reset-hold')
|
||||
.description('Reset budget hold for a month')
|
||||
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.resetBudgetHold(cmdOpts.month);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
72
packages/cli/src/commands/categories.ts
Normal file
72
packages/cli/src/commands/categories.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerCategoriesCommand(program: Command) {
|
||||
const categories = program
|
||||
.command('categories')
|
||||
.description('Manage categories');
|
||||
|
||||
categories
|
||||
.command('list')
|
||||
.description('List all categories')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getCategories();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
categories
|
||||
.command('create')
|
||||
.description('Create a new category')
|
||||
.requiredOption('--name <name>', 'Category name')
|
||||
.requiredOption('--group-id <id>', 'Category group ID')
|
||||
.option('--is-income', 'Mark as income category', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.createCategory({
|
||||
name: cmdOpts.name,
|
||||
group_id: cmdOpts.groupId,
|
||||
is_income: cmdOpts.isIncome,
|
||||
hidden: false,
|
||||
});
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
categories
|
||||
.command('update <id>')
|
||||
.description('Update a category')
|
||||
.option('--name <name>', 'New category name')
|
||||
.option('--hidden <bool>', 'Set hidden status')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (cmdOpts.name) fields.name = cmdOpts.name;
|
||||
if (cmdOpts.hidden !== undefined) {
|
||||
fields.hidden = cmdOpts.hidden === 'true';
|
||||
}
|
||||
await api.updateCategory(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
categories
|
||||
.command('delete <id>')
|
||||
.description('Delete a category')
|
||||
.option('--transfer-to <id>', 'Transfer transactions to this category')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteCategory(id, cmdOpts.transferTo);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
70
packages/cli/src/commands/category-groups.ts
Normal file
70
packages/cli/src/commands/category-groups.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerCategoryGroupsCommand(program: Command) {
|
||||
const groups = program
|
||||
.command('category-groups')
|
||||
.description('Manage category groups');
|
||||
|
||||
groups
|
||||
.command('list')
|
||||
.description('List all category groups')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getCategoryGroups();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
groups
|
||||
.command('create')
|
||||
.description('Create a new category group')
|
||||
.requiredOption('--name <name>', 'Group name')
|
||||
.option('--is-income', 'Mark as income group', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.createCategoryGroup({
|
||||
name: cmdOpts.name,
|
||||
is_income: cmdOpts.isIncome,
|
||||
hidden: false,
|
||||
});
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
groups
|
||||
.command('update <id>')
|
||||
.description('Update a category group')
|
||||
.option('--name <name>', 'New group name')
|
||||
.option('--hidden <bool>', 'Set hidden status')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (cmdOpts.name) fields.name = cmdOpts.name;
|
||||
if (cmdOpts.hidden !== undefined) {
|
||||
fields.hidden = cmdOpts.hidden === 'true';
|
||||
}
|
||||
await api.updateCategoryGroup(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
groups
|
||||
.command('delete <id>')
|
||||
.description('Delete a category group')
|
||||
.option('--transfer-to <id>', 'Transfer categories to this category')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteCategoryGroup(id, cmdOpts.transferTo);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
83
packages/cli/src/commands/payees.ts
Normal file
83
packages/cli/src/commands/payees.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerPayeesCommand(program: Command) {
|
||||
const payees = program.command('payees').description('Manage payees');
|
||||
|
||||
payees
|
||||
.command('list')
|
||||
.description('List all payees')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getPayees();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
payees
|
||||
.command('common')
|
||||
.description('List frequently used payees')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getCommonPayees();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
payees
|
||||
.command('create')
|
||||
.description('Create a new payee')
|
||||
.requiredOption('--name <name>', 'Payee name')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.createPayee({ name: cmdOpts.name });
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
payees
|
||||
.command('update <id>')
|
||||
.description('Update a payee')
|
||||
.option('--name <name>', 'New payee name')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (cmdOpts.name) fields.name = cmdOpts.name;
|
||||
await api.updatePayee(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
payees
|
||||
.command('delete <id>')
|
||||
.description('Delete a payee')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deletePayee(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
payees
|
||||
.command('merge')
|
||||
.description('Merge payees into a target payee')
|
||||
.requiredOption('--target <id>', 'Target payee ID')
|
||||
.requiredOption('--ids <ids>', 'Comma-separated payee IDs to merge')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const mergeIds = (cmdOpts.ids as string).split(',');
|
||||
await api.mergePayees(cmdOpts.target, mergeIds);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
76
packages/cli/src/commands/query.ts
Normal file
76
packages/cli/src/commands/query.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerQueryCommand(program: Command) {
|
||||
const query = program
|
||||
.command('query')
|
||||
.description('Run AQL (Actual Query Language) queries');
|
||||
|
||||
query
|
||||
.command('run')
|
||||
.description('Execute an AQL query')
|
||||
.option(
|
||||
'--table <table>',
|
||||
'Table to query (transactions, accounts, categories, payees)',
|
||||
)
|
||||
.option('--select <fields>', 'Comma-separated fields to select')
|
||||
.option('--filter <json>', 'Filter expression as JSON')
|
||||
.option('--order-by <fields>', 'Comma-separated fields to order by')
|
||||
.option('--limit <n>', 'Limit number of results')
|
||||
.option(
|
||||
'--file <path>',
|
||||
'Read full query object from JSON file (use - for stdin)',
|
||||
)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
let queryObj;
|
||||
|
||||
if (cmdOpts.file) {
|
||||
const content =
|
||||
cmdOpts.file === '-'
|
||||
? readFileSync(0, 'utf-8')
|
||||
: readFileSync(cmdOpts.file, 'utf-8');
|
||||
// If file contains a pre-built query, use it directly
|
||||
const parsed = JSON.parse(content);
|
||||
queryObj = api.q(parsed.table || cmdOpts.table || 'transactions');
|
||||
if (parsed.select) queryObj = queryObj.select(parsed.select);
|
||||
if (parsed.filter) queryObj = queryObj.filter(parsed.filter);
|
||||
if (parsed.orderBy) queryObj = queryObj.orderBy(parsed.orderBy);
|
||||
if (parsed.limit) queryObj = queryObj.limit(parsed.limit);
|
||||
} else {
|
||||
if (!cmdOpts.table) {
|
||||
throw new Error('--table is required (or use --file)');
|
||||
}
|
||||
queryObj = api.q(cmdOpts.table);
|
||||
|
||||
if (cmdOpts.select) {
|
||||
const fields = (cmdOpts.select as string).split(',');
|
||||
queryObj = queryObj.select(fields);
|
||||
}
|
||||
|
||||
if (cmdOpts.filter) {
|
||||
queryObj = queryObj.filter(JSON.parse(cmdOpts.filter));
|
||||
}
|
||||
|
||||
if (cmdOpts.orderBy) {
|
||||
const fields = (cmdOpts.orderBy as string).split(',');
|
||||
queryObj = queryObj.orderBy(fields);
|
||||
}
|
||||
|
||||
if (cmdOpts.limit) {
|
||||
queryObj = queryObj.limit(parseInt(cmdOpts.limit, 10));
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api.aqlQuery(queryObj);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
93
packages/cli/src/commands/rules.ts
Normal file
93
packages/cli/src/commands/rules.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
|
||||
if (cmdOpts.data) {
|
||||
return JSON.parse(cmdOpts.data);
|
||||
}
|
||||
if (cmdOpts.file) {
|
||||
const content =
|
||||
cmdOpts.file === '-'
|
||||
? readFileSync(0, 'utf-8')
|
||||
: readFileSync(cmdOpts.file, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
throw new Error('Either --data or --file is required');
|
||||
}
|
||||
|
||||
export function registerRulesCommand(program: Command) {
|
||||
const rules = program
|
||||
.command('rules')
|
||||
.description('Manage transaction rules');
|
||||
|
||||
rules
|
||||
.command('list')
|
||||
.description('List all rules')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getRules();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
rules
|
||||
.command('payee-rules <payeeId>')
|
||||
.description('List rules for a specific payee')
|
||||
.action(async (payeeId: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getPayeeRules(payeeId);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
rules
|
||||
.command('create')
|
||||
.description('Create a new rule')
|
||||
.option('--data <json>', 'Rule definition as JSON')
|
||||
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const rule = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.createRule
|
||||
>[0];
|
||||
const id = await api.createRule(rule);
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
rules
|
||||
.command('update')
|
||||
.description('Update a rule')
|
||||
.option('--data <json>', 'Rule data as JSON (must include id)')
|
||||
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const rule = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.updateRule
|
||||
>[0];
|
||||
await api.updateRule(rule);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
rules
|
||||
.command('delete <id>')
|
||||
.description('Delete a rule')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteRule(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
83
packages/cli/src/commands/schedules.ts
Normal file
83
packages/cli/src/commands/schedules.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
|
||||
if (cmdOpts.data) {
|
||||
return JSON.parse(cmdOpts.data);
|
||||
}
|
||||
if (cmdOpts.file) {
|
||||
const content =
|
||||
cmdOpts.file === '-'
|
||||
? readFileSync(0, 'utf-8')
|
||||
: readFileSync(cmdOpts.file, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
throw new Error('Either --data or --file is required');
|
||||
}
|
||||
|
||||
export function registerSchedulesCommand(program: Command) {
|
||||
const schedules = program
|
||||
.command('schedules')
|
||||
.description('Manage scheduled transactions');
|
||||
|
||||
schedules
|
||||
.command('list')
|
||||
.description('List all schedules')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getSchedules();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
schedules
|
||||
.command('create')
|
||||
.description('Create a new schedule')
|
||||
.option('--data <json>', 'Schedule definition as JSON')
|
||||
.option('--file <path>', 'Read schedule from JSON file (use - for stdin)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const schedule = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.createSchedule
|
||||
>[0];
|
||||
const id = await api.createSchedule(schedule);
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
schedules
|
||||
.command('update <id>')
|
||||
.description('Update a schedule')
|
||||
.option('--data <json>', 'Fields to update as JSON')
|
||||
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
|
||||
.option('--reset-next-date', 'Reset next occurrence date', false)
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.updateSchedule
|
||||
>[1];
|
||||
await api.updateSchedule(id, fields, cmdOpts.resetNextDate);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
schedules
|
||||
.command('delete <id>')
|
||||
.description('Delete a schedule')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteSchedule(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
55
packages/cli/src/commands/server.ts
Normal file
55
packages/cli/src/commands/server.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerServerCommand(program: Command) {
|
||||
const server = program.command('server').description('Server utilities');
|
||||
|
||||
server
|
||||
.command('version')
|
||||
.description('Get server version')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const version = await api.getServerVersion();
|
||||
printOutput({ version }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
server
|
||||
.command('get-id')
|
||||
.description('Get entity ID by name')
|
||||
.requiredOption(
|
||||
'--type <type>',
|
||||
'Entity type (accounts, categories, payees, schedules)',
|
||||
)
|
||||
.requiredOption('--name <name>', 'Entity name')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.getIDByName(cmdOpts.type, cmdOpts.name);
|
||||
printOutput(
|
||||
{ id, type: cmdOpts.type, name: cmdOpts.name },
|
||||
opts.format as OutputFormat,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
server
|
||||
.command('bank-sync')
|
||||
.description('Run bank synchronization')
|
||||
.option('--account <id>', 'Specific account ID to sync')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const args = cmdOpts.account
|
||||
? { accountId: cmdOpts.account }
|
||||
: undefined;
|
||||
await api.runBankSync(args);
|
||||
printOutput({ success: true }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
68
packages/cli/src/commands/tags.ts
Normal file
68
packages/cli/src/commands/tags.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
export function registerTagsCommand(program: Command) {
|
||||
const tags = program.command('tags').description('Manage tags');
|
||||
|
||||
tags
|
||||
.command('list')
|
||||
.description('List all tags')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getTags();
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
tags
|
||||
.command('create')
|
||||
.description('Create a new tag')
|
||||
.requiredOption('--tag <tag>', 'Tag name')
|
||||
.option('--color <color>', 'Tag color')
|
||||
.option('--description <description>', 'Tag description')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const id = await api.createTag({
|
||||
tag: cmdOpts.tag,
|
||||
color: cmdOpts.color,
|
||||
description: cmdOpts.description,
|
||||
});
|
||||
printOutput({ id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
tags
|
||||
.command('update <id>')
|
||||
.description('Update a tag')
|
||||
.option('--tag <tag>', 'New tag name')
|
||||
.option('--color <color>', 'New tag color')
|
||||
.option('--description <description>', 'New tag description')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields: Record<string, unknown> = {};
|
||||
if (cmdOpts.tag) fields.tag = cmdOpts.tag;
|
||||
if (cmdOpts.color) fields.color = cmdOpts.color;
|
||||
if (cmdOpts.description) fields.description = cmdOpts.description;
|
||||
await api.updateTag(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
tags
|
||||
.command('delete <id>')
|
||||
.description('Delete a tag')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteTag(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
130
packages/cli/src/commands/transactions.ts
Normal file
130
packages/cli/src/commands/transactions.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import * as api from '@actual-app/api';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { withConnection } from '../connection.js';
|
||||
import { printOutput } from '../output.js';
|
||||
import type { OutputFormat } from '../output.js';
|
||||
|
||||
function readJsonInput(cmdOpts: { data?: string; file?: string }): unknown {
|
||||
if (cmdOpts.data) {
|
||||
return JSON.parse(cmdOpts.data);
|
||||
}
|
||||
if (cmdOpts.file) {
|
||||
const content =
|
||||
cmdOpts.file === '-'
|
||||
? readFileSync(0, 'utf-8')
|
||||
: readFileSync(cmdOpts.file, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
throw new Error('Either --data or --file is required');
|
||||
}
|
||||
|
||||
export function registerTransactionsCommand(program: Command) {
|
||||
const transactions = program
|
||||
.command('transactions')
|
||||
.description('Manage transactions');
|
||||
|
||||
transactions
|
||||
.command('list')
|
||||
.description('List transactions for an account')
|
||||
.requiredOption('--account <id>', 'Account ID')
|
||||
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
|
||||
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const result = await api.getTransactions(
|
||||
cmdOpts.account,
|
||||
cmdOpts.start,
|
||||
cmdOpts.end,
|
||||
);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
transactions
|
||||
.command('add')
|
||||
.description('Add transactions to an account')
|
||||
.requiredOption('--account <id>', 'Account ID')
|
||||
.option('--data <json>', 'Transaction data as JSON array')
|
||||
.option(
|
||||
'--file <path>',
|
||||
'Read transaction data from JSON file (use - for stdin)',
|
||||
)
|
||||
.option('--learn-categories', 'Learn category assignments', false)
|
||||
.option('--run-transfers', 'Process transfers', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const transactions = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.addTransactions
|
||||
>[1];
|
||||
const result = await api.addTransactions(
|
||||
cmdOpts.account,
|
||||
transactions,
|
||||
{
|
||||
learnCategories: cmdOpts.learnCategories,
|
||||
runTransfers: cmdOpts.runTransfers,
|
||||
},
|
||||
);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
transactions
|
||||
.command('import')
|
||||
.description('Import transactions to an account')
|
||||
.requiredOption('--account <id>', 'Account ID')
|
||||
.option('--data <json>', 'Transaction data as JSON array')
|
||||
.option(
|
||||
'--file <path>',
|
||||
'Read transaction data from JSON file (use - for stdin)',
|
||||
)
|
||||
.option('--dry-run', 'Preview without importing', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const transactions = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.importTransactions
|
||||
>[1];
|
||||
const result = await api.importTransactions(
|
||||
cmdOpts.account,
|
||||
transactions,
|
||||
{
|
||||
defaultCleared: true,
|
||||
dryRun: cmdOpts.dryRun,
|
||||
},
|
||||
);
|
||||
printOutput(result, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
transactions
|
||||
.command('update <id>')
|
||||
.description('Update a transaction')
|
||||
.option('--data <json>', 'Fields to update as JSON')
|
||||
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
|
||||
.action(async (id: string, cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
const fields = readJsonInput(cmdOpts) as Parameters<
|
||||
typeof api.updateTransaction
|
||||
>[1];
|
||||
await api.updateTransaction(id, fields);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
|
||||
transactions
|
||||
.command('delete <id>')
|
||||
.description('Delete a transaction')
|
||||
.action(async (id: string) => {
|
||||
const opts = program.opts();
|
||||
await withConnection(opts, async () => {
|
||||
await api.deleteTransaction(id);
|
||||
printOutput({ success: true, id }, opts.format as OutputFormat);
|
||||
});
|
||||
});
|
||||
}
|
||||
86
packages/cli/src/config.ts
Normal file
86
packages/cli/src/config.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
import { cosmiconfig } from 'cosmiconfig';
|
||||
|
||||
export type CliConfig = {
|
||||
serverURL: string;
|
||||
password?: string;
|
||||
sessionToken?: string;
|
||||
budgetId?: string;
|
||||
dataDir: string;
|
||||
};
|
||||
|
||||
type CliGlobalOpts = {
|
||||
serverUrl?: string;
|
||||
password?: string;
|
||||
sessionToken?: string;
|
||||
budgetId?: string;
|
||||
dataDir?: string;
|
||||
};
|
||||
|
||||
type ConfigFileContent = {
|
||||
serverURL?: string;
|
||||
password?: string;
|
||||
sessionToken?: string;
|
||||
budgetId?: string;
|
||||
dataDir?: string;
|
||||
};
|
||||
|
||||
async function loadConfigFile(): Promise<ConfigFileContent> {
|
||||
const explorer = cosmiconfig('actual');
|
||||
const result = await explorer.search();
|
||||
if (result && !result.isEmpty) {
|
||||
return result.config as ConfigFileContent;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function resolveConfig(
|
||||
cliOpts: CliGlobalOpts,
|
||||
): Promise<CliConfig> {
|
||||
const fileConfig = await loadConfigFile();
|
||||
|
||||
const serverURL =
|
||||
cliOpts.serverUrl ??
|
||||
process.env.ACTUAL_SERVER_URL ??
|
||||
fileConfig.serverURL ??
|
||||
'';
|
||||
|
||||
const password =
|
||||
cliOpts.password ?? process.env.ACTUAL_PASSWORD ?? fileConfig.password;
|
||||
|
||||
const sessionToken =
|
||||
cliOpts.sessionToken ??
|
||||
process.env.ACTUAL_SESSION_TOKEN ??
|
||||
fileConfig.sessionToken;
|
||||
|
||||
const budgetId =
|
||||
cliOpts.budgetId ?? process.env.ACTUAL_BUDGET_ID ?? fileConfig.budgetId;
|
||||
|
||||
const dataDir =
|
||||
cliOpts.dataDir ??
|
||||
process.env.ACTUAL_DATA_DIR ??
|
||||
fileConfig.dataDir ??
|
||||
join(homedir(), '.actual-cli', 'data');
|
||||
|
||||
if (!serverURL) {
|
||||
throw new Error(
|
||||
'Server URL is required. Set --server-url, ACTUAL_SERVER_URL env var, or serverURL in config file.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!password && !sessionToken) {
|
||||
throw new Error(
|
||||
'Authentication required. Set --password/--session-token, ACTUAL_PASSWORD/ACTUAL_SESSION_TOKEN env var, or password/sessionToken in config file.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
serverURL,
|
||||
password,
|
||||
sessionToken,
|
||||
budgetId,
|
||||
dataDir,
|
||||
};
|
||||
}
|
||||
52
packages/cli/src/connection.ts
Normal file
52
packages/cli/src/connection.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as api from '@actual-app/api';
|
||||
|
||||
import { resolveConfig } from './config.js';
|
||||
import type { CliConfig } from './config.js';
|
||||
|
||||
type GlobalOpts = {
|
||||
serverUrl?: string;
|
||||
password?: string;
|
||||
sessionToken?: string;
|
||||
budgetId?: string;
|
||||
dataDir?: string;
|
||||
quiet?: boolean;
|
||||
};
|
||||
|
||||
function info(message: string, quiet?: boolean) {
|
||||
if (!quiet) {
|
||||
process.stderr.write(message + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
export async function withConnection<T>(
|
||||
globalOpts: GlobalOpts,
|
||||
fn: (config: CliConfig) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const config = await resolveConfig(globalOpts);
|
||||
|
||||
info(`Connecting to ${config.serverURL}...`, globalOpts.quiet);
|
||||
|
||||
if (config.sessionToken) {
|
||||
await api.init({
|
||||
serverURL: config.serverURL,
|
||||
sessionToken: config.sessionToken,
|
||||
dataDir: config.dataDir,
|
||||
});
|
||||
} else if (config.password) {
|
||||
await api.init({
|
||||
serverURL: config.serverURL,
|
||||
password: config.password,
|
||||
dataDir: config.dataDir,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (config.budgetId) {
|
||||
info(`Downloading budget ${config.budgetId}...`, globalOpts.quiet);
|
||||
await api.downloadBudget(config.budgetId);
|
||||
}
|
||||
return await fn(config);
|
||||
} finally {
|
||||
await api.shutdown();
|
||||
}
|
||||
}
|
||||
50
packages/cli/src/index.ts
Normal file
50
packages/cli/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { registerAccountsCommand } from './commands/accounts.js';
|
||||
import { registerBudgetsCommand } from './commands/budgets.js';
|
||||
import { registerCategoriesCommand } from './commands/categories.js';
|
||||
import { registerCategoryGroupsCommand } from './commands/category-groups.js';
|
||||
import { registerPayeesCommand } from './commands/payees.js';
|
||||
import { registerQueryCommand } from './commands/query.js';
|
||||
import { registerRulesCommand } from './commands/rules.js';
|
||||
import { registerSchedulesCommand } from './commands/schedules.js';
|
||||
import { registerServerCommand } from './commands/server.js';
|
||||
import { registerTagsCommand } from './commands/tags.js';
|
||||
import { registerTransactionsCommand } from './commands/transactions.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('actual')
|
||||
.description('CLI for Actual Budget')
|
||||
.version('26.3.0')
|
||||
.option('--server-url <url>', 'Actual server URL (env: ACTUAL_SERVER_URL)')
|
||||
.option('--password <password>', 'Server password (env: ACTUAL_PASSWORD)')
|
||||
.option(
|
||||
'--session-token <token>',
|
||||
'Session token (env: ACTUAL_SESSION_TOKEN)',
|
||||
)
|
||||
.option('--budget-id <id>', 'Budget ID to load (env: ACTUAL_BUDGET_ID)')
|
||||
.option('--data-dir <path>', 'Data directory (env: ACTUAL_DATA_DIR)')
|
||||
.option('--format <format>', 'Output format: json, table, csv', 'json')
|
||||
.option('--quiet', 'Suppress informational messages', false);
|
||||
|
||||
registerAccountsCommand(program);
|
||||
registerBudgetsCommand(program);
|
||||
registerCategoriesCommand(program);
|
||||
registerCategoryGroupsCommand(program);
|
||||
registerTransactionsCommand(program);
|
||||
registerPayeesCommand(program);
|
||||
registerTagsCommand(program);
|
||||
registerRulesCommand(program);
|
||||
registerSchedulesCommand(program);
|
||||
registerQueryCommand(program);
|
||||
registerServerCommand(program);
|
||||
|
||||
program.parseAsync(process.argv).catch((err: Error) => {
|
||||
process.stderr.write(`Error: ${err.message}\n`);
|
||||
process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
|
||||
process.exitCode = 1;
|
||||
});
|
||||
82
packages/cli/src/output.ts
Normal file
82
packages/cli/src/output.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import Table from 'cli-table3';
|
||||
|
||||
export type OutputFormat = 'json' | 'table' | 'csv';
|
||||
|
||||
export function formatOutput(
|
||||
data: unknown,
|
||||
format: OutputFormat = 'json',
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(data, null, 2);
|
||||
case 'table':
|
||||
return formatTable(data);
|
||||
case 'csv':
|
||||
return formatCsv(data);
|
||||
default:
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTable(data: unknown): string {
|
||||
if (!Array.isArray(data)) {
|
||||
if (data && typeof data === 'object') {
|
||||
const table = new Table();
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
table.push({ [key]: String(value) });
|
||||
}
|
||||
return table.toString();
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return '(no results)';
|
||||
}
|
||||
|
||||
const keys = Object.keys(data[0] as Record<string, unknown>);
|
||||
const table = new Table({ head: keys });
|
||||
|
||||
for (const row of data) {
|
||||
const r = row as Record<string, unknown>;
|
||||
table.push(keys.map(k => String(r[k] ?? '')));
|
||||
}
|
||||
|
||||
return table.toString();
|
||||
}
|
||||
|
||||
function formatCsv(data: unknown): string {
|
||||
if (!Array.isArray(data)) {
|
||||
if (data && typeof data === 'object') {
|
||||
const entries = Object.entries(data);
|
||||
const header = entries.map(([k]) => escapeCsv(k)).join(',');
|
||||
const values = entries.map(([, v]) => escapeCsv(String(v))).join(',');
|
||||
return header + '\n' + values;
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = Object.keys(data[0] as Record<string, unknown>);
|
||||
const header = keys.map(k => escapeCsv(k)).join(',');
|
||||
const rows = data.map(row => {
|
||||
const r = row as Record<string, unknown>;
|
||||
return keys.map(k => escapeCsv(String(r[k] ?? ''))).join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
function escapeCsv(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return '"' + value.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function printOutput(data: unknown, format: OutputFormat = 'json') {
|
||||
process.stdout.write(formatOutput(data, format) + '\n');
|
||||
}
|
||||
17
packages/cli/tsconfig.json
Normal file
17
packages/cli/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2021"],
|
||||
"types": ["node"],
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
},
|
||||
"references": [{ "path": "../api" }],
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "coverage"]
|
||||
}
|
||||
7
packages/cli/vitest.config.ts
Normal file
7
packages/cli/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
@@ -230,6 +230,7 @@ const sidebars = {
|
||||
link: { type: 'doc', id: 'api/index' },
|
||||
items: [
|
||||
'api/reference',
|
||||
'api/cli',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'ActualQL',
|
||||
|
||||
348
packages/docs/docs/api/cli.md
Normal file
348
packages/docs/docs/api/cli.md
Normal file
@@ -0,0 +1,348 @@
|
||||
---
|
||||
title: 'CLI'
|
||||
---
|
||||
|
||||
# CLI Tool
|
||||
|
||||
The `@actual-app/cli` package provides a command-line interface for interacting with your Actual Budget data. It connects to your sync server and lets you query and modify budgets, accounts, transactions, categories, payees, rules, schedules, and more — all from the terminal.
|
||||
|
||||
:::note
|
||||
This is different from the [sync-server CLI tool](../install/cli-tool.md) (`@actual-app/sync-server`), which is used to host and manage the Actual server itself.
|
||||
:::
|
||||
|
||||
## Installation
|
||||
|
||||
Node.js v22 or higher is required.
|
||||
|
||||
```bash
|
||||
npm install --save @actual-app/cli
|
||||
```
|
||||
|
||||
Or install globally:
|
||||
|
||||
```bash
|
||||
npm install --location=global @actual-app/cli
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI requires a connection to a running Actual sync server. Configuration can be provided via environment variables, CLI flags, or a config file.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------------- | ------------------------------------------ |
|
||||
| `ACTUAL_SERVER_URL` | URL of the Actual sync server (required) |
|
||||
| `ACTUAL_BUDGET_ID` | Budget ID to load (required for most commands) |
|
||||
| `ACTUAL_PASSWORD` | Server password (one of password or token required) |
|
||||
| `ACTUAL_SESSION_TOKEN` | Session token (alternative to password) |
|
||||
|
||||
### CLI Flags
|
||||
|
||||
Global flags override environment variables:
|
||||
|
||||
| Flag | Description |
|
||||
| --------------------------- | ----------------------------------------------- |
|
||||
| `--server-url <url>` | Server URL |
|
||||
| `--password <pw>` | Server password |
|
||||
| `--session-token <token>` | Session token |
|
||||
| `--budget-id <id>` | Budget ID |
|
||||
| `--data-dir <path>` | Local data directory for cached budget data |
|
||||
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
|
||||
| `--quiet` | Suppress informational messages on stderr |
|
||||
|
||||
### Config File
|
||||
|
||||
The CLI uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) for configuration. You can create a config file in any of these formats:
|
||||
|
||||
- `.actualrc` (JSON or YAML)
|
||||
- `.actualrc.json`, `.actualrc.yaml`, `.actualrc.yml`, `.actualrc.js`, `.actualrc.cjs`
|
||||
- `actual.config.js`, `actual.config.cjs`
|
||||
- An `"actual"` key in your `package.json`
|
||||
|
||||
Example `.actualrc.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverUrl": "http://localhost:5006",
|
||||
"password": "your-password",
|
||||
"budgetId": "1cfdbb80-6274-49bf-b0c2-737235a4c81f"
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
actual <command> <subcommand> [options]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Accounts
|
||||
|
||||
```bash
|
||||
# List all accounts
|
||||
actual accounts list
|
||||
|
||||
# Create an account
|
||||
actual accounts create --name "Checking" [--offbudget] [--balance 50000]
|
||||
|
||||
# Update an account
|
||||
actual accounts update <id> [--name "New Name"] [--offbudget true]
|
||||
|
||||
# Close an account (with optional transfer)
|
||||
actual accounts close <id> [--transfer-account <id>] [--transfer-category <id>]
|
||||
|
||||
# Reopen a closed account
|
||||
actual accounts reopen <id>
|
||||
|
||||
# Delete an account
|
||||
actual accounts delete <id>
|
||||
|
||||
# Get account balance
|
||||
actual accounts balance <id> [--cutoff 2026-01-31]
|
||||
```
|
||||
|
||||
### Budgets
|
||||
|
||||
```bash
|
||||
# List available budgets on the server
|
||||
actual budgets list
|
||||
|
||||
# Download a budget by sync ID
|
||||
actual budgets download <syncId> [--encryption-password <pw>]
|
||||
|
||||
# Sync the current budget
|
||||
actual budgets sync
|
||||
|
||||
# List budget months
|
||||
actual budgets months
|
||||
|
||||
# View a specific month
|
||||
actual budgets month 2026-03
|
||||
|
||||
# Set a budget amount (in integer cents)
|
||||
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
|
||||
|
||||
# Set carryover flag
|
||||
actual budgets set-carryover --month 2026-03 --category <id> --flag true
|
||||
|
||||
# Hold funds for next month
|
||||
actual budgets hold-next-month --month 2026-03 --amount 10000
|
||||
|
||||
# Reset held funds
|
||||
actual budgets reset-hold --month 2026-03
|
||||
```
|
||||
|
||||
### Categories
|
||||
|
||||
```bash
|
||||
# List all categories
|
||||
actual categories list
|
||||
|
||||
# Create a category
|
||||
actual categories create --name "Groceries" --group-id <id> [--is-income]
|
||||
|
||||
# Update a category
|
||||
actual categories update <id> [--name "Food"] [--hidden true]
|
||||
|
||||
# Delete a category (with optional transfer)
|
||||
actual categories delete <id> [--transfer-to <id>]
|
||||
```
|
||||
|
||||
### Category Groups
|
||||
|
||||
```bash
|
||||
# List all category groups
|
||||
actual category-groups list
|
||||
|
||||
# Create a category group
|
||||
actual category-groups create --name "Essentials" [--is-income]
|
||||
|
||||
# Update a category group
|
||||
actual category-groups update <id> [--name "New Name"] [--hidden true]
|
||||
|
||||
# Delete a category group (with optional transfer)
|
||||
actual category-groups delete <id> [--transfer-to <id>]
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```bash
|
||||
# List transactions for an account within a date range
|
||||
actual transactions list --account <id> --start 2026-01-01 --end 2026-03-31
|
||||
|
||||
# Add transactions (inline JSON)
|
||||
actual transactions add --account <id> --data '[{"date":"2026-03-13","amount":-5000,"payee_name":"Store"}]'
|
||||
|
||||
# Add transactions (from file)
|
||||
actual transactions add --account <id> --file transactions.json
|
||||
|
||||
# Import transactions with reconciliation (deduplication)
|
||||
actual transactions import --account <id> --data '[...]' [--dry-run]
|
||||
|
||||
# Update a transaction
|
||||
actual transactions update <id> --data '{"notes":"Updated note"}'
|
||||
|
||||
# Delete a transaction
|
||||
actual transactions delete <id>
|
||||
```
|
||||
|
||||
### Payees
|
||||
|
||||
```bash
|
||||
# List all payees
|
||||
actual payees list
|
||||
|
||||
# List common payees
|
||||
actual payees common
|
||||
|
||||
# Create a payee
|
||||
actual payees create --name "Grocery Store"
|
||||
|
||||
# Update a payee
|
||||
actual payees update <id> --name "New Name"
|
||||
|
||||
# Delete a payee
|
||||
actual payees delete <id>
|
||||
|
||||
# Merge multiple payees into one
|
||||
actual payees merge --target <id> --ids id1,id2,id3
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
```bash
|
||||
# List all tags
|
||||
actual tags list
|
||||
|
||||
# Create a tag
|
||||
actual tags create --tag "vacation" [--color "#ff0000"] [--description "Vacation expenses"]
|
||||
|
||||
# Update a tag
|
||||
actual tags update <id> [--tag "trip"] [--color "#00ff00"]
|
||||
|
||||
# Delete a tag
|
||||
actual tags delete <id>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
```bash
|
||||
# List all rules
|
||||
actual rules list
|
||||
|
||||
# List rules for a specific payee
|
||||
actual rules payee-rules <payeeId>
|
||||
|
||||
# Create a rule (inline JSON)
|
||||
actual rules create --data '{"stage":"pre","conditionsOp":"and","conditions":[...],"actions":[...]}'
|
||||
|
||||
# Create a rule (from file)
|
||||
actual rules create --file rule.json
|
||||
|
||||
# Update a rule
|
||||
actual rules update --data '{"id":"...","stage":"pre",...}'
|
||||
|
||||
# Delete a rule
|
||||
actual rules delete <id>
|
||||
```
|
||||
|
||||
### Schedules
|
||||
|
||||
```bash
|
||||
# List all schedules
|
||||
actual schedules list
|
||||
|
||||
# Create a schedule
|
||||
actual schedules create --data '{"name":"Rent","date":"1st","amount":-150000,"amountOp":"is","account":"...","payee":"..."}'
|
||||
|
||||
# Update a schedule
|
||||
actual schedules update <id> --data '{"name":"Updated Rent"}' [--reset-next-date]
|
||||
|
||||
# Delete a schedule
|
||||
actual schedules delete <id>
|
||||
```
|
||||
|
||||
### Query (ActualQL)
|
||||
|
||||
Run queries using [ActualQL](./actual-ql/index.md):
|
||||
|
||||
```bash
|
||||
# Run a query (inline)
|
||||
actual query run --table transactions --select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
|
||||
|
||||
# Run a query (from file)
|
||||
actual query run --file query.json
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
```bash
|
||||
# Get the server version
|
||||
actual server version
|
||||
|
||||
# Look up an entity ID by name
|
||||
actual server get-id --type accounts --name "Checking"
|
||||
actual server get-id --type categories --name "Groceries"
|
||||
|
||||
# Trigger bank sync
|
||||
actual server bank-sync [--account <id>]
|
||||
```
|
||||
|
||||
## Amount Convention
|
||||
|
||||
All monetary amounts are represented as **integer cents**:
|
||||
|
||||
| CLI Value | Dollar Amount |
|
||||
| --------- | ------------- |
|
||||
| `5000` | $50.00 |
|
||||
| `-12350` | -$123.50 |
|
||||
| `100` | $1.00 |
|
||||
|
||||
When providing amounts, always use integer cents. For example, to budget $50, pass `5000`.
|
||||
|
||||
## Output Formats
|
||||
|
||||
The `--format` flag controls how results are displayed:
|
||||
|
||||
- **`json`** (default) — Machine-readable JSON output, ideal for scripting
|
||||
- **`table`** — Human-readable table format
|
||||
- **`csv`** — Comma-separated values for spreadsheet import
|
||||
|
||||
Use `--quiet` to suppress informational messages on stderr when piping output to other tools.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**View your budget for the current month:**
|
||||
|
||||
```bash
|
||||
actual budgets month 2026-03 --format table
|
||||
```
|
||||
|
||||
**Check an account balance:**
|
||||
|
||||
```bash
|
||||
# Find the account ID
|
||||
actual server get-id --type accounts --name "Checking" --quiet
|
||||
# Get the balance
|
||||
actual accounts balance <id> --quiet
|
||||
```
|
||||
|
||||
**Export transactions to CSV:**
|
||||
|
||||
```bash
|
||||
actual transactions list --account <id> --start 2026-01-01 --end 2026-12-31 --format csv > transactions.csv
|
||||
```
|
||||
|
||||
**Add a transaction:**
|
||||
|
||||
```bash
|
||||
actual transactions add --account <id> --data '[{"date":"2026-03-14","amount":-2500,"payee_name":"Coffee Shop"}]'
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Non-zero exit codes indicate an error
|
||||
- Error output is JSON on stdout: `{"error": "message"}`
|
||||
- Use `--quiet` to suppress informational stderr messages without affecting error output
|
||||
@@ -7,7 +7,8 @@
|
||||
{ "path": "./packages/api" },
|
||||
{ "path": "./packages/desktop-client" },
|
||||
{ "path": "./packages/sync-server" },
|
||||
{ "path": "./packages/desktop-electron" }
|
||||
{ "path": "./packages/desktop-electron" },
|
||||
{ "path": "./packages/cli" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@@ -19,7 +19,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@actual-app/api@workspace:packages/api":
|
||||
"@actual-app/api@workspace:^, @actual-app/api@workspace:packages/api":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/api@workspace:packages/api"
|
||||
dependencies:
|
||||
@@ -49,6 +49,24 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@actual-app/cli@workspace:packages/cli":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/cli@workspace:packages/cli"
|
||||
dependencies:
|
||||
"@actual-app/api": "workspace:^"
|
||||
"@types/node": "npm:^22.19.10"
|
||||
cli-table3: "npm:^0.6.5"
|
||||
commander: "npm:^13.0.0"
|
||||
cosmiconfig: "npm:^9.0.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
typescript-strict-plugin: "npm:^2.4.4"
|
||||
vitest: "npm:^4.0.18"
|
||||
bin:
|
||||
actual: ./build/src/index.js
|
||||
actual-cli: ./build/src/index.js
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@actual-app/components@workspace:*, @actual-app/components@workspace:packages/component-library":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/components@workspace:packages/component-library"
|
||||
@@ -13182,7 +13200,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-table3@npm:^0.6.3":
|
||||
"cli-table3@npm:^0.6.3, cli-table3@npm:^0.6.5":
|
||||
version: 0.6.5
|
||||
resolution: "cli-table3@npm:0.6.5"
|
||||
dependencies:
|
||||
@@ -13492,6 +13510,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^13.0.0":
|
||||
version: 13.1.0
|
||||
resolution: "commander@npm:13.1.0"
|
||||
checksum: 10/d3b4b79e6be8471ddadacbb8cd441fe82154d7da7393b50e76165a9e29ccdb74fa911a186437b9a211d0fc071db6051915c94fb8ef16d77511d898e9dbabc6af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^14.0.0, commander@npm:^14.0.2":
|
||||
version: 14.0.2
|
||||
resolution: "commander@npm:14.0.2"
|
||||
@@ -13892,6 +13917,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cosmiconfig@npm:^9.0.0":
|
||||
version: 9.0.1
|
||||
resolution: "cosmiconfig@npm:9.0.1"
|
||||
dependencies:
|
||||
env-paths: "npm:^2.2.1"
|
||||
import-fresh: "npm:^3.3.0"
|
||||
js-yaml: "npm:^4.1.0"
|
||||
parse-json: "npm:^5.2.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.9.5"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10/89fcac84d062f0710091bb2d6a6175bcde22f5448877db9c43429694408191d3d4e215193b3ac4d54f7f89ef188d55cd481c7a2295b0dc572e65b528bf6fec01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crc@npm:^3.8.0":
|
||||
version: 3.8.0
|
||||
resolution: "crc@npm:3.8.0"
|
||||
@@ -15678,7 +15720,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"env-paths@npm:^2.2.0":
|
||||
"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "env-paths@npm:2.2.1"
|
||||
checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e
|
||||
|
||||
Reference in New Issue
Block a user