@slothmoney/agent-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +159 -41
- package/dist/args.js +302 -2
- package/dist/cli.js +607 -5
- package/dist/contracts.js +96 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.0 - 2026-07-30
|
|
6
|
+
|
|
7
|
+
- Add command-specific help for every command and auth subcommand, including
|
|
8
|
+
required inputs, option constraints, output, examples, and write safety.
|
|
9
|
+
- Explain that categories are parents and line items are scoped children, and
|
|
10
|
+
that line-item labels may repeat across categories.
|
|
11
|
+
- Clarify local native credential storage and environment-only authentication
|
|
12
|
+
for containers, CI, and other headless systems.
|
|
13
|
+
- Read and preview or apply the linked joint-budget shared-transaction setting.
|
|
14
|
+
- Filter transactions by personal or joint assignment scope.
|
|
15
|
+
- Categorise shared personal-account contributions against the joint catalogue
|
|
16
|
+
with `assignmentScope: "joint"`.
|
|
17
|
+
- Validate the nested `jointBudgetContribution` response contract.
|
|
18
|
+
- Add goal listing, creation, partial updates, and deletion through the Agent
|
|
19
|
+
API, with goal document ID validation and typed response validation.
|
|
20
|
+
- Keep goal writes in preview mode unless `--apply` is provided, including
|
|
21
|
+
explicit amount and month clearing.
|
|
22
|
+
- Explain that active shared goal target amounts remain app-managed so balance
|
|
23
|
+
allocations can be recalculated safely.
|
|
24
|
+
|
|
3
25
|
## 0.2.0 - 2026-07-23
|
|
4
26
|
|
|
5
27
|
- Add `auth login`, `auth status`, and `auth logout`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to
|
|
3
|
+
Use your own agent to manage goals and categorise transactions through the
|
|
4
4
|
[Sloth Money Agent API](https://slothmoney.app/developers/).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -15,14 +15,19 @@ sloth-agent --version
|
|
|
15
15
|
For a one-off pinned run:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm exec --yes --package=@slothmoney/agent-cli@0.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.3.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
22
22
|
|
|
23
23
|
Create a personal access token in Sloth Money under
|
|
24
|
-
**Settings > Developer access**, then
|
|
25
|
-
|
|
24
|
+
**Settings > Developer access**, then choose the authentication method for
|
|
25
|
+
where the CLI runs.
|
|
26
|
+
|
|
27
|
+
### Local computer
|
|
28
|
+
|
|
29
|
+
On an interactive desktop, save the token in your operating system's secure
|
|
30
|
+
credential store:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
sloth-agent auth login
|
|
@@ -31,36 +36,45 @@ sloth-agent auth login
|
|
|
31
36
|
The prompt hides the token. The CLI validates it before replacing any
|
|
32
37
|
credential already stored for the selected API origin.
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
the environment:
|
|
39
|
+
### Containers, CI, and headless systems
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Never put a token in a command argument. For CI and headless systems, keep
|
|
43
|
-
using an environment secret:
|
|
41
|
+
Native credential storage may be unavailable in a container or other headless
|
|
42
|
+
environment. Inject `SLOTH_AGENT_TOKEN` at runtime through your platform's
|
|
43
|
+
secret manager:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
export SLOTH_AGENT_TOKEN="sloth_pat_v1_..."
|
|
47
|
+
sloth-agent auth status
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
Keep using the environment variable for later commands. You do not need to run
|
|
51
|
+
`sloth-agent auth login` in this setup. Do not put the token in a command
|
|
52
|
+
argument, source file, or container image.
|
|
53
|
+
|
|
49
54
|
`SLOTH_AGENT_TOKEN` always overrides a stored credential.
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
### Import an existing environment token
|
|
57
|
+
|
|
58
|
+
To save an environment token in native credential storage on a local computer:
|
|
52
59
|
|
|
53
60
|
```bash
|
|
54
61
|
sloth-agent auth login --from-env
|
|
55
62
|
unset SLOTH_AGENT_TOKEN
|
|
56
|
-
sloth-agent auth status
|
|
57
|
-
sloth-agent categories
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
You can also pass a token to the login command through stdin:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
printf '%s' "$SLOTH_AGENT_TOKEN" | sloth-agent auth login --token-stdin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both commands validate the token before replacing the stored credential.
|
|
72
|
+
|
|
73
|
+
Check the active credential and read your categories:
|
|
61
74
|
|
|
62
75
|
```bash
|
|
63
76
|
sloth-agent auth status
|
|
77
|
+
sloth-agent categories
|
|
64
78
|
```
|
|
65
79
|
|
|
66
80
|
This updates the PAT's `lastUsedAt` value. To remove the local credential:
|
|
@@ -74,59 +88,163 @@ remotely in **Sloth Money Settings > Developer access**.
|
|
|
74
88
|
|
|
75
89
|
## Commands
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
An assignment categorises an existing transaction e.g. assigning category
|
|
92
|
+
Groceries to a transaction.
|
|
93
|
+
|
|
94
|
+
Every command has built-in reference documentation covering its inputs,
|
|
95
|
+
options, output, and examples:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
sloth-agent auth login --help
|
|
99
|
+
sloth-agent categories --help
|
|
100
|
+
sloth-agent transactions --help
|
|
101
|
+
sloth-agent assign --help
|
|
102
|
+
sloth-agent joint-budget-settings --help
|
|
103
|
+
sloth-agent goals create --help
|
|
104
|
+
sloth-agent goals update --help
|
|
105
|
+
sloth-agent ask-partner --help
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Categorise a transaction end to end
|
|
109
|
+
|
|
110
|
+
1. Read categories and available budget line items:
|
|
78
111
|
|
|
79
112
|
```bash
|
|
80
113
|
sloth-agent categories
|
|
81
114
|
```
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
A category is the broader parent. A line item is a child within one category.
|
|
117
|
+
Line-item names such as `Other` may repeat, so preserve the full choice as
|
|
118
|
+
`(scope, categoryId, lineItemId)`. Use the personal or joint line-item map that
|
|
119
|
+
matches the transaction scope. For example, `Bills → Other` and `Subscriptions
|
|
120
|
+
→ Other` are different choices.
|
|
121
|
+
|
|
122
|
+
2. Read uncategorised transactions:
|
|
84
123
|
|
|
85
124
|
```bash
|
|
86
125
|
sloth-agent transactions --uncategorized --limit 50
|
|
87
126
|
```
|
|
88
127
|
|
|
89
|
-
|
|
128
|
+
3. Copy the exact `transactionRef` for the transaction and a `categoryId` from
|
|
129
|
+
the earlier outputs into `assignments.json`:
|
|
90
130
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"assignments": [
|
|
134
|
+
{
|
|
135
|
+
"transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",
|
|
136
|
+
"categoryId": "PASTE_A_CATEGORY_ID_HERE"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
96
140
|
```
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
These are placeholders. Do not submit the example values.
|
|
143
|
+
|
|
144
|
+
4. Preview the assignment without writing:
|
|
99
145
|
|
|
100
146
|
```bash
|
|
101
147
|
sloth-agent assign --input assignments.json
|
|
102
148
|
```
|
|
103
149
|
|
|
104
|
-
|
|
150
|
+
Without `--apply`, the CLI checks that the file is valid and returns the
|
|
151
|
+
payload it would send. It does not contact Sloth Money, verify the
|
|
152
|
+
`transactionRef` or category values, or write anything. A successful preview
|
|
153
|
+
does not guarantee that applying it will succeed.
|
|
154
|
+
|
|
155
|
+
5. Apply the same file:
|
|
105
156
|
|
|
106
157
|
```bash
|
|
107
158
|
sloth-agent assign --input assignments.json --apply
|
|
108
159
|
```
|
|
109
160
|
|
|
110
|
-
|
|
161
|
+
Inspect every item in the returned `succeeded` and `failed` arrays.
|
|
162
|
+
|
|
163
|
+
6. Check the result. Successful assignments update the category and optional
|
|
164
|
+
budget line item on the original transaction. See the result in **Sloth
|
|
165
|
+
Money → Transactions**, or re-run the original transaction query without
|
|
166
|
+
`--uncategorized` and inspect its category fields:
|
|
111
167
|
|
|
112
168
|
```bash
|
|
113
|
-
sloth-agent
|
|
169
|
+
sloth-agent transactions --limit 50
|
|
114
170
|
```
|
|
115
171
|
|
|
116
|
-
|
|
172
|
+
The transaction should also disappear from the matching `--uncategorized`
|
|
173
|
+
query. Assignments do not create a separate list.
|
|
117
174
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
### Other workflows
|
|
176
|
+
|
|
177
|
+
List your goals:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
sloth-agent goals
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Goal writes are previews unless `--apply` is present:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
sloth-agent goals create \
|
|
187
|
+
--name "Emergency fund" \
|
|
188
|
+
--target-amount 12000 \
|
|
189
|
+
--target-month 2027-06
|
|
190
|
+
|
|
191
|
+
sloth-agent goals create \
|
|
192
|
+
--name "Emergency fund" \
|
|
193
|
+
--target-amount 12000 \
|
|
194
|
+
--target-month 2027-06 \
|
|
195
|
+
--apply
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use the `id` from list or create output to update or delete a goal:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
sloth-agent goals update \
|
|
202
|
+
--goal-id goal-id \
|
|
203
|
+
--clear-target-amount \
|
|
204
|
+
--target-month 2027-12 \
|
|
205
|
+
--achieved=false \
|
|
206
|
+
--apply
|
|
207
|
+
|
|
208
|
+
sloth-agent goals delete --goal-id goal-id --apply
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Updates are partial. Use `--clear-target-amount` or `--clear-target-month` to
|
|
212
|
+
remove an optional value. Marking a goal achieved removes its forecast
|
|
213
|
+
assignment. Deleting a goal also removes its forecast assignments and drift
|
|
214
|
+
history. Goal sharing remains app-managed. Change an active shared goal's
|
|
215
|
+
pot-tracked target amount in the Sloth Budget app, where account balances can
|
|
216
|
+
be reallocated across goals in priority order.
|
|
217
|
+
|
|
218
|
+
Read uncategorised contributions to the joint budget:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
sloth-agent transactions --assignment-scope joint --uncategorized
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Set `"assignmentScope": "joint"` on an assignment to categorise the eligible
|
|
225
|
+
shared portion for the joint budget.
|
|
226
|
+
|
|
227
|
+
Set whether the shared portions of personal-account transactions count in the
|
|
228
|
+
linked joint budget. The first command previews; the second applies:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
sloth-agent joint-budget-settings \
|
|
232
|
+
--include-shared-personal-transactions=true
|
|
233
|
+
sloth-agent joint-budget-settings \
|
|
234
|
+
--include-shared-personal-transactions=true \
|
|
235
|
+
--apply
|
|
128
236
|
```
|
|
129
237
|
|
|
238
|
+
Create a partner clarification link:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
sloth-agent ask-partner \
|
|
242
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The value shown is a placeholder. Copy the exact `transactionRef` from
|
|
246
|
+
`sloth-agent transactions` output.
|
|
247
|
+
|
|
130
248
|
## Configuration and output
|
|
131
249
|
|
|
132
250
|
The CLI defaults to `https://budget.slothmoney.app`. For local development,
|
|
@@ -144,7 +262,7 @@ Command results are JSON on stdout. Diagnostics are written to stderr.
|
|
|
144
262
|
| --- | --- |
|
|
145
263
|
| `0` | Success |
|
|
146
264
|
| `1` | API, network, credential-store, response-validation, or partial assignment failure |
|
|
147
|
-
| `2` | Invalid command, option, URL, date, auth input, or assignment input |
|
|
265
|
+
| `2` | Invalid command, option, URL, date, auth input, goal input, or assignment input |
|
|
148
266
|
| `3` | No credential or native secure storage is unavailable |
|
|
149
267
|
|
|
150
268
|
Assignment writes are best-effort. A response containing any failed assignment
|
package/dist/args.js
CHANGED
|
@@ -47,6 +47,47 @@ function requireNonEmpty(value, name) {
|
|
|
47
47
|
throw new UsageError(`${name} requires a value`);
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
|
+
function parseGoalAmount(value, name) {
|
|
51
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
52
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
53
|
+
}
|
|
54
|
+
const amount = Number(value);
|
|
55
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
56
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
57
|
+
}
|
|
58
|
+
return amount;
|
|
59
|
+
}
|
|
60
|
+
function parseGoalMonthKey(value, name) {
|
|
61
|
+
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
|
62
|
+
throw new UsageError(`${name} must be a valid YYYY-MM month`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseExplicitBoolean(value, name) {
|
|
67
|
+
if (value !== 'true' && value !== 'false') {
|
|
68
|
+
throw new UsageError(`${name} must be true or false`);
|
|
69
|
+
}
|
|
70
|
+
return value === 'true';
|
|
71
|
+
}
|
|
72
|
+
function parseGoalName(value) {
|
|
73
|
+
const name = value.trim();
|
|
74
|
+
if (name.length > 200) {
|
|
75
|
+
throw new UsageError('--name must be at most 200 characters');
|
|
76
|
+
}
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
function parseGoalId(value) {
|
|
80
|
+
const goalId = value.trim();
|
|
81
|
+
if (goalId.length > 500
|
|
82
|
+
|| goalId.includes('/')
|
|
83
|
+
|| Array.from(goalId).some((character) => {
|
|
84
|
+
const codePoint = character.codePointAt(0);
|
|
85
|
+
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
86
|
+
})) {
|
|
87
|
+
throw new UsageError('--goal-id must be a valid goal document ID');
|
|
88
|
+
}
|
|
89
|
+
return goalId;
|
|
90
|
+
}
|
|
50
91
|
function parseTransactions(args) {
|
|
51
92
|
const filters = {};
|
|
52
93
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -73,6 +114,7 @@ function parseTransactions(args) {
|
|
|
73
114
|
'--q',
|
|
74
115
|
'--account-id',
|
|
75
116
|
'--category-id',
|
|
117
|
+
'--assignment-scope',
|
|
76
118
|
'--cursor',
|
|
77
119
|
]);
|
|
78
120
|
if (!name || !supported.has(name)) {
|
|
@@ -108,6 +150,12 @@ function parseTransactions(args) {
|
|
|
108
150
|
else if (name === '--category-id') {
|
|
109
151
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
110
152
|
}
|
|
153
|
+
else if (name === '--assignment-scope') {
|
|
154
|
+
if (value !== 'personal' && value !== 'joint') {
|
|
155
|
+
throw new UsageError('--assignment-scope must be personal or joint');
|
|
156
|
+
}
|
|
157
|
+
filters.assignmentScope = setOnce(filters.assignmentScope, value, name);
|
|
158
|
+
}
|
|
111
159
|
else if (name === '--cursor') {
|
|
112
160
|
filters.cursor = setOnce(filters.cursor, value, name);
|
|
113
161
|
}
|
|
@@ -162,9 +210,221 @@ function parseAuth(args, baseUrl) {
|
|
|
162
210
|
}
|
|
163
211
|
throw new UsageError(`Unknown auth command: ${authCommand}`);
|
|
164
212
|
}
|
|
213
|
+
function parseGoals(args, baseUrl) {
|
|
214
|
+
const subcommand = args.shift();
|
|
215
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
216
|
+
if (args.length > 0) {
|
|
217
|
+
throw new UsageError(`Unknown goals list option: ${args[0]}`);
|
|
218
|
+
}
|
|
219
|
+
return withBaseUrl({ command: 'goals-list' }, baseUrl);
|
|
220
|
+
}
|
|
221
|
+
if (subcommand === 'create') {
|
|
222
|
+
let name;
|
|
223
|
+
let targetAmount;
|
|
224
|
+
let targetMonthKey;
|
|
225
|
+
let apply = false;
|
|
226
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
227
|
+
const argument = args[index];
|
|
228
|
+
if (argument === '--apply') {
|
|
229
|
+
if (apply)
|
|
230
|
+
throw new UsageError('--apply may only be provided once');
|
|
231
|
+
apply = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const [option, inlineValue] = argument.includes('=')
|
|
235
|
+
? argument.split(/=(.*)/s, 2)
|
|
236
|
+
: [argument, undefined];
|
|
237
|
+
if (option !== '--name'
|
|
238
|
+
&& option !== '--target-amount'
|
|
239
|
+
&& option !== '--target-month') {
|
|
240
|
+
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
241
|
+
}
|
|
242
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
243
|
+
if (inlineValue === undefined)
|
|
244
|
+
index += 1;
|
|
245
|
+
if (option === '--name') {
|
|
246
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
247
|
+
}
|
|
248
|
+
else if (option === '--target-amount') {
|
|
249
|
+
targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!name)
|
|
256
|
+
throw new UsageError('goals create requires --name <name>');
|
|
257
|
+
return withBaseUrl({
|
|
258
|
+
command: 'goals-create',
|
|
259
|
+
name,
|
|
260
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
261
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
262
|
+
apply,
|
|
263
|
+
}, baseUrl);
|
|
264
|
+
}
|
|
265
|
+
if (subcommand === 'update') {
|
|
266
|
+
let goalId;
|
|
267
|
+
let name;
|
|
268
|
+
let targetAmount;
|
|
269
|
+
let targetMonthKey;
|
|
270
|
+
let isAchieved;
|
|
271
|
+
let apply = false;
|
|
272
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
273
|
+
const argument = args[index];
|
|
274
|
+
if (argument === '--apply') {
|
|
275
|
+
if (apply)
|
|
276
|
+
throw new UsageError('--apply may only be provided once');
|
|
277
|
+
apply = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (argument === '--clear-target-amount') {
|
|
281
|
+
if (targetAmount !== undefined) {
|
|
282
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
283
|
+
}
|
|
284
|
+
targetAmount = null;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (argument === '--clear-target-month') {
|
|
288
|
+
if (targetMonthKey !== undefined) {
|
|
289
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
290
|
+
}
|
|
291
|
+
targetMonthKey = null;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [option, inlineValue] = argument.includes('=')
|
|
295
|
+
? argument.split(/=(.*)/s, 2)
|
|
296
|
+
: [argument, undefined];
|
|
297
|
+
if (option !== '--goal-id'
|
|
298
|
+
&& option !== '--name'
|
|
299
|
+
&& option !== '--target-amount'
|
|
300
|
+
&& option !== '--target-month'
|
|
301
|
+
&& option !== '--achieved') {
|
|
302
|
+
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
303
|
+
}
|
|
304
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
305
|
+
if (inlineValue === undefined)
|
|
306
|
+
index += 1;
|
|
307
|
+
if (option === '--goal-id') {
|
|
308
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
309
|
+
}
|
|
310
|
+
else if (option === '--name') {
|
|
311
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
312
|
+
}
|
|
313
|
+
else if (option === '--target-amount') {
|
|
314
|
+
if (targetAmount !== undefined) {
|
|
315
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
316
|
+
}
|
|
317
|
+
targetAmount = parseGoalAmount(value, option);
|
|
318
|
+
}
|
|
319
|
+
else if (option === '--target-month') {
|
|
320
|
+
if (targetMonthKey !== undefined) {
|
|
321
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
322
|
+
}
|
|
323
|
+
targetMonthKey = parseGoalMonthKey(value, option);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!goalId)
|
|
330
|
+
throw new UsageError('goals update requires --goal-id <id>');
|
|
331
|
+
if (name === undefined
|
|
332
|
+
&& targetAmount === undefined
|
|
333
|
+
&& targetMonthKey === undefined
|
|
334
|
+
&& isAchieved === undefined) {
|
|
335
|
+
throw new UsageError('goals update requires at least one field to update');
|
|
336
|
+
}
|
|
337
|
+
return withBaseUrl({
|
|
338
|
+
command: 'goals-update',
|
|
339
|
+
goalId,
|
|
340
|
+
...(name === undefined ? {} : { name }),
|
|
341
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
342
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
343
|
+
...(isAchieved === undefined ? {} : { isAchieved }),
|
|
344
|
+
apply,
|
|
345
|
+
}, baseUrl);
|
|
346
|
+
}
|
|
347
|
+
if (subcommand === 'delete') {
|
|
348
|
+
let goalId;
|
|
349
|
+
let apply = false;
|
|
350
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
351
|
+
const argument = args[index];
|
|
352
|
+
if (argument === '--apply') {
|
|
353
|
+
if (apply)
|
|
354
|
+
throw new UsageError('--apply may only be provided once');
|
|
355
|
+
apply = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const [option, inlineValue] = argument.includes('=')
|
|
359
|
+
? argument.split(/=(.*)/s, 2)
|
|
360
|
+
: [argument, undefined];
|
|
361
|
+
if (option !== '--goal-id') {
|
|
362
|
+
throw new UsageError(`Unknown goals delete option: ${argument}`);
|
|
363
|
+
}
|
|
364
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
365
|
+
if (inlineValue === undefined)
|
|
366
|
+
index += 1;
|
|
367
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
368
|
+
}
|
|
369
|
+
if (!goalId)
|
|
370
|
+
throw new UsageError('goals delete requires --goal-id <id>');
|
|
371
|
+
return withBaseUrl({
|
|
372
|
+
command: 'goals-delete',
|
|
373
|
+
goalId,
|
|
374
|
+
apply,
|
|
375
|
+
}, baseUrl);
|
|
376
|
+
}
|
|
377
|
+
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
378
|
+
}
|
|
379
|
+
function helpTopic(argv) {
|
|
380
|
+
const positionals = [];
|
|
381
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
382
|
+
const argument = argv[index];
|
|
383
|
+
if (argument === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (argument === '--base-url') {
|
|
386
|
+
index += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (argument.startsWith('--base-url=') || argument.startsWith('-')) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
positionals.push(argument);
|
|
393
|
+
}
|
|
394
|
+
const [command, subcommand] = positionals;
|
|
395
|
+
if (command === 'auth') {
|
|
396
|
+
if (subcommand === 'login'
|
|
397
|
+
|| subcommand === 'status'
|
|
398
|
+
|| subcommand === 'logout') {
|
|
399
|
+
return `auth-${subcommand}`;
|
|
400
|
+
}
|
|
401
|
+
return 'auth';
|
|
402
|
+
}
|
|
403
|
+
if (command === 'goals') {
|
|
404
|
+
if (subcommand === 'list')
|
|
405
|
+
return 'goals-list';
|
|
406
|
+
if (subcommand === 'create')
|
|
407
|
+
return 'goals-create';
|
|
408
|
+
if (subcommand === 'update')
|
|
409
|
+
return 'goals-update';
|
|
410
|
+
if (subcommand === 'delete')
|
|
411
|
+
return 'goals-delete';
|
|
412
|
+
return 'goals';
|
|
413
|
+
}
|
|
414
|
+
if (command === 'categories'
|
|
415
|
+
|| command === 'transactions'
|
|
416
|
+
|| command === 'assign'
|
|
417
|
+
|| command === 'joint-budget-settings'
|
|
418
|
+
|| command === 'ask-partner') {
|
|
419
|
+
return command;
|
|
420
|
+
}
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
165
423
|
export function parseArgs(argv) {
|
|
166
|
-
if (argv.includes('--help') || argv.includes('-h'))
|
|
167
|
-
|
|
424
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
425
|
+
const topic = helpTopic(argv);
|
|
426
|
+
return topic ? { command: 'help', topic } : { command: 'help' };
|
|
427
|
+
}
|
|
168
428
|
if (argv.includes('--version') || argv.includes('-V'))
|
|
169
429
|
return { command: 'version' };
|
|
170
430
|
const { args, baseUrl } = parseGlobalOptions(argv);
|
|
@@ -174,6 +434,9 @@ export function parseArgs(argv) {
|
|
|
174
434
|
if (command === 'auth') {
|
|
175
435
|
return parseAuth(args, baseUrl);
|
|
176
436
|
}
|
|
437
|
+
if (command === 'goals') {
|
|
438
|
+
return parseGoals(args, baseUrl);
|
|
439
|
+
}
|
|
177
440
|
if (command === 'categories') {
|
|
178
441
|
if (args.length > 0) {
|
|
179
442
|
throw new UsageError(`Unknown categories option: ${args[0]}`);
|
|
@@ -208,6 +471,43 @@ export function parseArgs(argv) {
|
|
|
208
471
|
throw new UsageError('assign requires --input <file>');
|
|
209
472
|
return withBaseUrl({ command, input, apply }, baseUrl);
|
|
210
473
|
}
|
|
474
|
+
if (command === 'joint-budget-settings') {
|
|
475
|
+
let includeSharedPersonalTransactions;
|
|
476
|
+
let apply = false;
|
|
477
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
478
|
+
const argument = args[index];
|
|
479
|
+
if (argument === '--apply') {
|
|
480
|
+
if (apply)
|
|
481
|
+
throw new UsageError('--apply may only be provided once');
|
|
482
|
+
apply = true;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const optionName = '--include-shared-personal-transactions';
|
|
486
|
+
if (argument === optionName || argument.startsWith(`${optionName}=`)) {
|
|
487
|
+
const value = argument === optionName
|
|
488
|
+
? readOptionValue(args, index, optionName)
|
|
489
|
+
: argument.slice(`${optionName}=`.length);
|
|
490
|
+
if (argument === optionName)
|
|
491
|
+
index += 1;
|
|
492
|
+
if (value !== 'true' && value !== 'false') {
|
|
493
|
+
throw new UsageError(`${optionName} must be true or false`);
|
|
494
|
+
}
|
|
495
|
+
includeSharedPersonalTransactions = setOnce(includeSharedPersonalTransactions, value === 'true', optionName);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
throw new UsageError(`Unknown joint-budget-settings option: ${argument}`);
|
|
499
|
+
}
|
|
500
|
+
if (apply && includeSharedPersonalTransactions === undefined) {
|
|
501
|
+
throw new UsageError('--apply requires --include-shared-personal-transactions=true|false');
|
|
502
|
+
}
|
|
503
|
+
return withBaseUrl({
|
|
504
|
+
command,
|
|
505
|
+
...(includeSharedPersonalTransactions === undefined
|
|
506
|
+
? {}
|
|
507
|
+
: { includeSharedPersonalTransactions }),
|
|
508
|
+
apply,
|
|
509
|
+
}, baseUrl);
|
|
510
|
+
}
|
|
211
511
|
if (command === 'ask-partner') {
|
|
212
512
|
let transactionRef;
|
|
213
513
|
for (let index = 0; index < args.length; index += 1) {
|