@stephendolan/ynab-cli 1.2.5 → 1.2.6
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/dist/cli.js +469 -426
- package/dist/cli.js.map +1 -1
- package/package.json +6 -5
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/lib/utils.ts
|
|
7
|
-
import { format, parseISO } from "date-fns";
|
|
8
|
-
|
|
9
6
|
// src/lib/errors.ts
|
|
10
7
|
var YnabCliError = class extends Error {
|
|
11
8
|
constructor(message, statusCode) {
|
|
@@ -162,7 +159,10 @@ function parseStatusFilter(value) {
|
|
|
162
159
|
const validStatuses = ["cleared", "uncleared", "reconciled"];
|
|
163
160
|
for (const status of statuses) {
|
|
164
161
|
if (!validStatuses.includes(status)) {
|
|
165
|
-
throw new YnabCliError(
|
|
162
|
+
throw new YnabCliError(
|
|
163
|
+
`Invalid status '${status}'. Must be one of: ${validStatuses.join(", ")}`,
|
|
164
|
+
400
|
|
165
|
+
);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
return statuses;
|
|
@@ -196,11 +196,11 @@ function applyFieldSelection(items, fields) {
|
|
|
196
196
|
return items.map((item) => {
|
|
197
197
|
const filtered = {};
|
|
198
198
|
const itemRecord = item;
|
|
199
|
-
|
|
199
|
+
for (const field of fieldList) {
|
|
200
200
|
if (field in itemRecord) {
|
|
201
201
|
filtered[field] = itemRecord[field];
|
|
202
202
|
}
|
|
203
|
-
}
|
|
203
|
+
}
|
|
204
204
|
return filtered;
|
|
205
205
|
});
|
|
206
206
|
}
|
|
@@ -475,14 +475,6 @@ var YnabClient = class {
|
|
|
475
475
|
return response.data.account;
|
|
476
476
|
});
|
|
477
477
|
}
|
|
478
|
-
async createAccount(accountData, budgetId) {
|
|
479
|
-
return this.withErrorHandling(async () => {
|
|
480
|
-
const api = await this.getApi();
|
|
481
|
-
const id = await this.getBudgetId(budgetId);
|
|
482
|
-
const response = await api.accounts.createAccount(id, accountData);
|
|
483
|
-
return response.data.account;
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
478
|
async getCategories(budgetId, lastKnowledgeOfServer) {
|
|
487
479
|
return this.withErrorHandling(async () => {
|
|
488
480
|
const api = await this.getApi();
|
|
@@ -502,22 +494,6 @@ var YnabClient = class {
|
|
|
502
494
|
return response.data.category;
|
|
503
495
|
});
|
|
504
496
|
}
|
|
505
|
-
async updateCategory(categoryId, data, budgetId) {
|
|
506
|
-
return this.withErrorHandling(async () => {
|
|
507
|
-
const api = await this.getApi();
|
|
508
|
-
const id = await this.getBudgetId(budgetId);
|
|
509
|
-
const response = await api.categories.updateCategory(id, categoryId, data);
|
|
510
|
-
return response.data.category;
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
async getMonthCategory(month, categoryId, budgetId) {
|
|
514
|
-
return this.withErrorHandling(async () => {
|
|
515
|
-
const api = await this.getApi();
|
|
516
|
-
const id = await this.getBudgetId(budgetId);
|
|
517
|
-
const response = await api.categories.getMonthCategoryById(id, month, categoryId);
|
|
518
|
-
return response.data.category;
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
497
|
async updateMonthCategory(month, categoryId, data, budgetId) {
|
|
522
498
|
return this.withErrorHandling(async () => {
|
|
523
499
|
const api = await this.getApi();
|
|
@@ -553,22 +529,6 @@ var YnabClient = class {
|
|
|
553
529
|
return response.data.payee;
|
|
554
530
|
});
|
|
555
531
|
}
|
|
556
|
-
async getPayeeLocations(budgetId) {
|
|
557
|
-
return this.withErrorHandling(async () => {
|
|
558
|
-
const api = await this.getApi();
|
|
559
|
-
const id = await this.getBudgetId(budgetId);
|
|
560
|
-
const response = await api.payeeLocations.getPayeeLocations(id);
|
|
561
|
-
return response.data.payee_locations;
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
async getPayeeLocation(payeeLocationId, budgetId) {
|
|
565
|
-
return this.withErrorHandling(async () => {
|
|
566
|
-
const api = await this.getApi();
|
|
567
|
-
const id = await this.getBudgetId(budgetId);
|
|
568
|
-
const response = await api.payeeLocations.getPayeeLocationById(id, payeeLocationId);
|
|
569
|
-
return response.data.payee_location;
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
532
|
async getPayeeLocationsByPayee(payeeId, budgetId) {
|
|
573
533
|
return this.withErrorHandling(async () => {
|
|
574
534
|
const api = await this.getApi();
|
|
@@ -679,34 +639,14 @@ var YnabClient = class {
|
|
|
679
639
|
return response.data.transaction;
|
|
680
640
|
});
|
|
681
641
|
}
|
|
682
|
-
async createTransactions(transactionsData, budgetId) {
|
|
683
|
-
return this.withErrorHandling(async () => {
|
|
684
|
-
const api = await this.getApi();
|
|
685
|
-
const id = await this.getBudgetId(budgetId);
|
|
686
|
-
const response = await api.transactions.createTransactions(id, transactionsData);
|
|
687
|
-
return response.data;
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
642
|
async updateTransaction(transactionId, transactionData, budgetId) {
|
|
691
643
|
return this.withErrorHandling(async () => {
|
|
692
644
|
const api = await this.getApi();
|
|
693
645
|
const id = await this.getBudgetId(budgetId);
|
|
694
|
-
const response = await api.transactions.updateTransaction(
|
|
695
|
-
id,
|
|
696
|
-
transactionId,
|
|
697
|
-
transactionData
|
|
698
|
-
);
|
|
646
|
+
const response = await api.transactions.updateTransaction(id, transactionId, transactionData);
|
|
699
647
|
return response.data.transaction;
|
|
700
648
|
});
|
|
701
649
|
}
|
|
702
|
-
async updateTransactions(transactionsData, budgetId) {
|
|
703
|
-
return this.withErrorHandling(async () => {
|
|
704
|
-
const api = await this.getApi();
|
|
705
|
-
const id = await this.getBudgetId(budgetId);
|
|
706
|
-
const response = await api.transactions.updateTransactions(id, transactionsData);
|
|
707
|
-
return response.data;
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
650
|
async deleteTransaction(transactionId, budgetId) {
|
|
711
651
|
return this.withErrorHandling(async () => {
|
|
712
652
|
const api = await this.getApi();
|
|
@@ -748,26 +688,6 @@ var YnabClient = class {
|
|
|
748
688
|
return response.data.scheduled_transaction;
|
|
749
689
|
});
|
|
750
690
|
}
|
|
751
|
-
async createScheduledTransaction(data, budgetId) {
|
|
752
|
-
return this.withErrorHandling(async () => {
|
|
753
|
-
const api = await this.getApi();
|
|
754
|
-
const id = await this.getBudgetId(budgetId);
|
|
755
|
-
const response = await api.scheduledTransactions.createScheduledTransaction(id, data);
|
|
756
|
-
return response.data.scheduled_transaction;
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
async updateScheduledTransaction(scheduledTransactionId, data, budgetId) {
|
|
760
|
-
return this.withErrorHandling(async () => {
|
|
761
|
-
const api = await this.getApi();
|
|
762
|
-
const id = await this.getBudgetId(budgetId);
|
|
763
|
-
const response = await api.scheduledTransactions.updateScheduledTransaction(
|
|
764
|
-
id,
|
|
765
|
-
scheduledTransactionId,
|
|
766
|
-
data
|
|
767
|
-
);
|
|
768
|
-
return response.data.scheduled_transaction;
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
691
|
async deleteScheduledTransaction(scheduledTransactionId, budgetId) {
|
|
772
692
|
return this.withErrorHandling(async () => {
|
|
773
693
|
const api = await this.getApi();
|
|
@@ -846,40 +766,46 @@ function buildUpdateObject(options, mapping) {
|
|
|
846
766
|
// src/commands/auth.ts
|
|
847
767
|
function createAuthCommand() {
|
|
848
768
|
const cmd = new Command("auth").description("Authentication management");
|
|
849
|
-
cmd.command("login").description("Configure access token").action(
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
try {
|
|
854
|
-
const user = await client.getUser();
|
|
855
|
-
outputJson({
|
|
856
|
-
message: "Successfully authenticated",
|
|
857
|
-
user: { id: user?.id }
|
|
858
|
-
});
|
|
859
|
-
} catch (error) {
|
|
860
|
-
await auth.deleteAccessToken();
|
|
769
|
+
cmd.command("login").description("Configure access token").action(
|
|
770
|
+
withErrorHandling(async () => {
|
|
771
|
+
const token = await promptForAccessToken();
|
|
772
|
+
await auth.setAccessToken(token);
|
|
861
773
|
client.clearApi();
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
774
|
+
try {
|
|
775
|
+
const user = await client.getUser();
|
|
776
|
+
outputJson({
|
|
777
|
+
message: "Successfully authenticated",
|
|
778
|
+
user: { id: user?.id }
|
|
779
|
+
});
|
|
780
|
+
} catch (error) {
|
|
781
|
+
await auth.deleteAccessToken();
|
|
782
|
+
client.clearApi();
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
cmd.command("status").description("Check authentication status").action(
|
|
788
|
+
withErrorHandling(async () => {
|
|
789
|
+
const isAuthenticated = await auth.isAuthenticated();
|
|
790
|
+
if (!isAuthenticated) {
|
|
791
|
+
outputJson({ authenticated: false, message: "Not authenticated" });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
const user = await client.getUser();
|
|
796
|
+
outputJson({ authenticated: true, user: { id: user?.id } });
|
|
797
|
+
} catch {
|
|
798
|
+
outputJson({ authenticated: false, message: "Token exists but is invalid" });
|
|
799
|
+
}
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
cmd.command("logout").description("Remove stored credentials").action(
|
|
803
|
+
withErrorHandling(async () => {
|
|
804
|
+
await auth.logout();
|
|
805
|
+
client.clearApi();
|
|
806
|
+
outputJson({ message: "Successfully logged out" });
|
|
807
|
+
})
|
|
808
|
+
);
|
|
883
809
|
return cmd;
|
|
884
810
|
}
|
|
885
811
|
|
|
@@ -887,10 +813,12 @@ function createAuthCommand() {
|
|
|
887
813
|
import { Command as Command2 } from "commander";
|
|
888
814
|
function createUserCommand() {
|
|
889
815
|
const cmd = new Command2("user").description("User information");
|
|
890
|
-
cmd.command("info").description("Get authenticated user information").action(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
816
|
+
cmd.command("info").description("Get authenticated user information").action(
|
|
817
|
+
withErrorHandling(async () => {
|
|
818
|
+
const user = await client.getUser();
|
|
819
|
+
outputJson(user);
|
|
820
|
+
})
|
|
821
|
+
);
|
|
894
822
|
return cmd;
|
|
895
823
|
}
|
|
896
824
|
|
|
@@ -898,30 +826,38 @@ function createUserCommand() {
|
|
|
898
826
|
import { Command as Command3 } from "commander";
|
|
899
827
|
function createBudgetsCommand() {
|
|
900
828
|
const cmd = new Command3("budgets").description("Budget operations");
|
|
901
|
-
cmd.command("list").description("List all budgets").option("--include-accounts", "Include accounts in response").action(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
cmd.command("
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
budget
|
|
923
|
-
|
|
924
|
-
|
|
829
|
+
cmd.command("list").description("List all budgets").option("--include-accounts", "Include accounts in response").action(
|
|
830
|
+
withErrorHandling(async (options) => {
|
|
831
|
+
const result = await client.getBudgets(options.includeAccounts);
|
|
832
|
+
outputJson(result?.budgets);
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
cmd.command("view").description("View budget details (uses default if no id provided)").argument("[id]", "Budget ID").action(
|
|
836
|
+
withErrorHandling(async (id) => {
|
|
837
|
+
const result = await client.getBudget(id);
|
|
838
|
+
outputJson(result?.budget);
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
cmd.command("settings").description("View budget settings").argument("[id]", "Budget ID").action(
|
|
842
|
+
withErrorHandling(async (id) => {
|
|
843
|
+
const settings = await client.getBudgetSettings(id);
|
|
844
|
+
outputJson(settings);
|
|
845
|
+
})
|
|
846
|
+
);
|
|
847
|
+
cmd.command("set-default").description("Set default budget for commands").argument("<id>", "Budget ID").action(
|
|
848
|
+
withErrorHandling(async (id) => {
|
|
849
|
+
const result = await client.getBudgets();
|
|
850
|
+
const budget = result?.budgets.find((b) => b.id === id);
|
|
851
|
+
if (!budget) {
|
|
852
|
+
throw new YnabCliError(`Budget with ID ${id} not found`, 404);
|
|
853
|
+
}
|
|
854
|
+
config.setDefaultBudget(id);
|
|
855
|
+
outputJson({
|
|
856
|
+
message: "Default budget set",
|
|
857
|
+
budget: { id: budget.id, name: budget.name }
|
|
858
|
+
});
|
|
859
|
+
})
|
|
860
|
+
);
|
|
925
861
|
return cmd;
|
|
926
862
|
}
|
|
927
863
|
|
|
@@ -929,22 +865,30 @@ function createBudgetsCommand() {
|
|
|
929
865
|
import { Command as Command4 } from "commander";
|
|
930
866
|
function createAccountsCommand() {
|
|
931
867
|
const cmd = new Command4("accounts").description("Account operations");
|
|
932
|
-
cmd.command("list").description("List all accounts").option("-b, --budget <id>", "Budget ID").action(
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
868
|
+
cmd.command("list").description("List all accounts").option("-b, --budget <id>", "Budget ID").action(
|
|
869
|
+
withErrorHandling(async (options) => {
|
|
870
|
+
const result = await client.getAccounts(options.budget);
|
|
871
|
+
outputJson(result?.accounts);
|
|
872
|
+
})
|
|
873
|
+
);
|
|
874
|
+
cmd.command("view").description("View account details").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").action(
|
|
875
|
+
withErrorHandling(async (id, options) => {
|
|
876
|
+
const account = await client.getAccount(id, options.budget);
|
|
877
|
+
outputJson(account);
|
|
878
|
+
})
|
|
879
|
+
);
|
|
880
|
+
cmd.command("transactions").description("List transactions for account").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").action(
|
|
881
|
+
withErrorHandling(
|
|
882
|
+
async (id, options) => {
|
|
883
|
+
const result = await client.getTransactionsByAccount(id, {
|
|
884
|
+
budgetId: options.budget,
|
|
885
|
+
sinceDate: options.since,
|
|
886
|
+
type: options.type
|
|
887
|
+
});
|
|
888
|
+
outputJson(result?.transactions);
|
|
889
|
+
}
|
|
890
|
+
)
|
|
891
|
+
);
|
|
948
892
|
return cmd;
|
|
949
893
|
}
|
|
950
894
|
|
|
@@ -952,36 +896,50 @@ function createAccountsCommand() {
|
|
|
952
896
|
import { Command as Command5 } from "commander";
|
|
953
897
|
function createCategoriesCommand() {
|
|
954
898
|
const cmd = new Command5("categories").description("Category operations");
|
|
955
|
-
cmd.command("list").description("List all categories").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
cmd.command("
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
899
|
+
cmd.command("list").description("List all categories").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
900
|
+
withErrorHandling(
|
|
901
|
+
async (options) => {
|
|
902
|
+
const result = await client.getCategories(options.budget, options.lastKnowledge);
|
|
903
|
+
outputJson(result?.category_groups);
|
|
904
|
+
}
|
|
905
|
+
)
|
|
906
|
+
);
|
|
907
|
+
cmd.command("view").description("View category details").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").action(
|
|
908
|
+
withErrorHandling(async (id, options) => {
|
|
909
|
+
const category = await client.getCategory(id, options.budget);
|
|
910
|
+
outputJson(category);
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
cmd.command("budget").description("Set category budgeted amount for a month (overrides existing amount)").argument("<id>", "Category ID").requiredOption("--month <month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").requiredOption("--amount <amount>", "Total budgeted amount to set (e.g., 100.50)", parseFloat).option("-b, --budget <id>", "Budget ID").action(
|
|
914
|
+
withErrorHandling(
|
|
915
|
+
async (id, options) => {
|
|
916
|
+
if (isNaN(options.amount)) {
|
|
917
|
+
throw new YnabCliError("Amount must be a valid number", 400);
|
|
918
|
+
}
|
|
919
|
+
const milliunits = amountToMilliunits(options.amount);
|
|
920
|
+
const category = await client.updateMonthCategory(
|
|
921
|
+
options.month,
|
|
922
|
+
id,
|
|
923
|
+
{ category: { budgeted: milliunits } },
|
|
924
|
+
options.budget
|
|
925
|
+
);
|
|
926
|
+
outputJson(category);
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
);
|
|
930
|
+
cmd.command("transactions").description("List transactions for category").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
931
|
+
withErrorHandling(
|
|
932
|
+
async (id, options) => {
|
|
933
|
+
const result = await client.getTransactionsByCategory(id, {
|
|
934
|
+
budgetId: options.budget,
|
|
935
|
+
sinceDate: options.since,
|
|
936
|
+
type: options.type,
|
|
937
|
+
lastKnowledgeOfServer: options.lastKnowledge
|
|
938
|
+
});
|
|
939
|
+
outputJson(result?.transactions);
|
|
940
|
+
}
|
|
941
|
+
)
|
|
942
|
+
);
|
|
985
943
|
return cmd;
|
|
986
944
|
}
|
|
987
945
|
|
|
@@ -1033,161 +991,207 @@ function buildTransactionData(options) {
|
|
|
1033
991
|
}
|
|
1034
992
|
function createTransactionsCommand() {
|
|
1035
993
|
const cmd = new Command6("transactions").description("Transaction operations");
|
|
1036
|
-
cmd.command("list").description("List transactions").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Filter by account ID").option("--category <id>", "Filter by category ID").option("--payee <id>", "Filter by payee ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--until <date>", "Filter transactions until date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--approved <value>", "Filter by approval status: true or false").option(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
options.budget
|
|
1067
|
-
);
|
|
1068
|
-
outputJson(transaction);
|
|
1069
|
-
}));
|
|
1070
|
-
cmd.command("update").description("Update transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status").option("--approved", "Mark as approved").action(withErrorHandling(async (id, options) => {
|
|
1071
|
-
const transactionData = buildUpdateObject(options, {
|
|
1072
|
-
account: "account_id",
|
|
1073
|
-
date: "date",
|
|
1074
|
-
payeeName: "payee_name",
|
|
1075
|
-
payeeId: "payee_id",
|
|
1076
|
-
categoryId: "category_id",
|
|
1077
|
-
memo: "memo",
|
|
1078
|
-
cleared: "cleared",
|
|
1079
|
-
approved: "approved"
|
|
1080
|
-
});
|
|
1081
|
-
if (options.amount !== void 0) {
|
|
1082
|
-
transactionData.amount = amountToMilliunits(options.amount);
|
|
1083
|
-
}
|
|
1084
|
-
const transaction = await client.updateTransaction(
|
|
1085
|
-
id,
|
|
1086
|
-
{ transaction: transactionData },
|
|
1087
|
-
options.budget
|
|
1088
|
-
);
|
|
1089
|
-
outputJson(transaction);
|
|
1090
|
-
}));
|
|
1091
|
-
cmd.command("delete").description("Delete transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
|
|
1092
|
-
if (!await confirmDelete("transaction", options.yes)) {
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
const transaction = await client.deleteTransaction(id, options.budget);
|
|
1096
|
-
outputJson({ message: "Transaction deleted", transaction });
|
|
1097
|
-
}));
|
|
1098
|
-
cmd.command("import").description("Import transactions").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (options) => {
|
|
1099
|
-
const transactionIds = await client.importTransactions(options.budget);
|
|
1100
|
-
outputJson({ transaction_ids: transactionIds });
|
|
1101
|
-
}));
|
|
1102
|
-
cmd.command("split").description("Split transaction into multiple categories. Amounts should be in dollars (e.g., 10.50).").argument("<id>", "Transaction ID").requiredOption("--splits <json>", 'JSON array of splits with dollar amounts: [{"amount": -21.40, "category_id": "xxx", "memo": "..."}]').option("-b, --budget <id>", "Budget ID").option("-f, --force", "Force update of already-split transactions by deleting and recreating").action(withErrorHandling(async (id, options) => {
|
|
1103
|
-
let parsedSplits;
|
|
1104
|
-
try {
|
|
1105
|
-
parsedSplits = JSON.parse(options.splits);
|
|
1106
|
-
} catch (error) {
|
|
1107
|
-
throw new YnabCliError("Invalid JSON in --splits parameter", 400);
|
|
1108
|
-
}
|
|
1109
|
-
const splits = validateJson(parsedSplits, TransactionSplitSchema, "transaction splits");
|
|
1110
|
-
const splitsInMilliunits = splits.map((split) => ({
|
|
1111
|
-
...split,
|
|
1112
|
-
amount: amountToMilliunits(split.amount)
|
|
1113
|
-
}));
|
|
1114
|
-
const existingTransaction = await client.getTransaction(id, options.budget);
|
|
1115
|
-
const isAlreadySplit = existingTransaction.subtransactions && existingTransaction.subtransactions.length > 0;
|
|
1116
|
-
if (isAlreadySplit && !options.force) {
|
|
1117
|
-
throw new YnabCliError(
|
|
1118
|
-
"Transaction is already split. YNAB API does not support updating split transactions. Use --force to delete and recreate the transaction with new splits.",
|
|
1119
|
-
400
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
if (isAlreadySplit) {
|
|
1123
|
-
await client.deleteTransaction(id, options.budget);
|
|
1124
|
-
const recreatedTransaction = await client.createTransaction(
|
|
1125
|
-
{
|
|
1126
|
-
transaction: {
|
|
1127
|
-
account_id: existingTransaction.account_id,
|
|
1128
|
-
date: existingTransaction.date,
|
|
1129
|
-
amount: existingTransaction.amount,
|
|
1130
|
-
payee_id: existingTransaction.payee_id,
|
|
1131
|
-
payee_name: existingTransaction.payee_name,
|
|
1132
|
-
category_id: null,
|
|
1133
|
-
memo: existingTransaction.memo,
|
|
1134
|
-
cleared: existingTransaction.cleared,
|
|
1135
|
-
approved: existingTransaction.approved,
|
|
1136
|
-
flag_color: existingTransaction.flag_color,
|
|
1137
|
-
subtransactions: splitsInMilliunits
|
|
1138
|
-
}
|
|
1139
|
-
},
|
|
1140
|
-
options.budget
|
|
1141
|
-
);
|
|
1142
|
-
outputJson(recreatedTransaction);
|
|
1143
|
-
} else {
|
|
1144
|
-
const transaction = await client.updateTransaction(
|
|
1145
|
-
id,
|
|
1146
|
-
{
|
|
1147
|
-
transaction: {
|
|
1148
|
-
category_id: null,
|
|
1149
|
-
subtransactions: splitsInMilliunits
|
|
1150
|
-
}
|
|
1151
|
-
},
|
|
1152
|
-
options.budget
|
|
1153
|
-
);
|
|
994
|
+
cmd.command("list").description("List transactions").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Filter by account ID").option("--category <id>", "Filter by category ID").option("--payee <id>", "Filter by payee ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--until <date>", "Filter transactions until date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--approved <value>", "Filter by approval status: true or false").option(
|
|
995
|
+
"--status <statuses>",
|
|
996
|
+
"Filter by cleared status: cleared, uncleared, reconciled (comma-separated for multiple)"
|
|
997
|
+
).option("--min-amount <amount>", "Minimum amount in currency units (e.g., 10.50)", parseFloat).option("--max-amount <amount>", "Maximum amount in currency units (e.g., 100.00)", parseFloat).option(
|
|
998
|
+
"--fields <fields>",
|
|
999
|
+
"Comma-separated list of fields to include (e.g., id,date,amount,memo)"
|
|
1000
|
+
).action(
|
|
1001
|
+
withErrorHandling(
|
|
1002
|
+
async (options) => {
|
|
1003
|
+
const params = {
|
|
1004
|
+
budgetId: options.budget,
|
|
1005
|
+
sinceDate: options.since,
|
|
1006
|
+
type: options.type
|
|
1007
|
+
};
|
|
1008
|
+
const result = options.account ? await client.getTransactionsByAccount(options.account, params) : options.category ? await client.getTransactionsByCategory(options.category, params) : options.payee ? await client.getTransactionsByPayee(options.payee, params) : await client.getTransactions(params);
|
|
1009
|
+
const transactions = result?.transactions || [];
|
|
1010
|
+
const filtered = applyTransactionFilters(transactions, {
|
|
1011
|
+
until: options.until,
|
|
1012
|
+
approved: options.approved,
|
|
1013
|
+
status: options.status,
|
|
1014
|
+
minAmount: options.minAmount,
|
|
1015
|
+
maxAmount: options.maxAmount
|
|
1016
|
+
});
|
|
1017
|
+
const selected = applyFieldSelection(filtered, options.fields);
|
|
1018
|
+
outputJson(selected);
|
|
1019
|
+
}
|
|
1020
|
+
)
|
|
1021
|
+
);
|
|
1022
|
+
cmd.command("view").description("View single transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").action(
|
|
1023
|
+
withErrorHandling(async (id, options) => {
|
|
1024
|
+
const transaction = await client.getTransaction(id, options.budget);
|
|
1154
1025
|
outputJson(transaction);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
cmd.command("
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1026
|
+
})
|
|
1027
|
+
);
|
|
1028
|
+
cmd.command("create").description("Create transaction").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units (e.g., 10.50)", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status (cleared, uncleared, reconciled)").option("--approved", "Mark as approved").action(
|
|
1029
|
+
withErrorHandling(
|
|
1030
|
+
async (options) => {
|
|
1031
|
+
const shouldPrompt = isInteractive() && !options.amount;
|
|
1032
|
+
if (shouldPrompt && !options.account) {
|
|
1033
|
+
throw new YnabCliError(
|
|
1034
|
+
"--account is required. Interactive mode cannot auto-select an account.",
|
|
1035
|
+
400
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const transactionData = shouldPrompt ? { ...await promptForTransaction(), account_id: options.account } : buildTransactionData(options);
|
|
1039
|
+
const transaction = await client.createTransaction(
|
|
1040
|
+
{ transaction: transactionData },
|
|
1041
|
+
options.budget
|
|
1042
|
+
);
|
|
1043
|
+
outputJson(transaction);
|
|
1044
|
+
}
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
cmd.command("update").description("Update transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("--account <id>", "Account ID").option("--date <date>", "Date (YYYY-MM-DD)").option("--amount <amount>", "Amount in currency units", parseFloat).option("--payee-name <name>", "Payee name").option("--payee-id <id>", "Payee ID").option("--category-id <id>", "Category ID").option("--memo <memo>", "Memo").option("--cleared <status>", "Cleared status").option("--approved", "Mark as approved").action(
|
|
1048
|
+
withErrorHandling(
|
|
1049
|
+
async (id, options) => {
|
|
1050
|
+
const transactionData = buildUpdateObject(options, {
|
|
1051
|
+
account: "account_id",
|
|
1052
|
+
date: "date",
|
|
1053
|
+
payeeName: "payee_name",
|
|
1054
|
+
payeeId: "payee_id",
|
|
1055
|
+
categoryId: "category_id",
|
|
1056
|
+
memo: "memo",
|
|
1057
|
+
cleared: "cleared",
|
|
1058
|
+
approved: "approved"
|
|
1059
|
+
});
|
|
1060
|
+
if (options.amount !== void 0) {
|
|
1061
|
+
transactionData.amount = amountToMilliunits(options.amount);
|
|
1062
|
+
}
|
|
1063
|
+
const transaction = await client.updateTransaction(
|
|
1064
|
+
id,
|
|
1065
|
+
{ transaction: transactionData },
|
|
1066
|
+
options.budget
|
|
1067
|
+
);
|
|
1068
|
+
outputJson(transaction);
|
|
1069
|
+
}
|
|
1070
|
+
)
|
|
1071
|
+
);
|
|
1072
|
+
cmd.command("delete").description("Delete transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(
|
|
1073
|
+
withErrorHandling(
|
|
1074
|
+
async (id, options) => {
|
|
1075
|
+
if (!await confirmDelete("transaction", options.yes)) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const transaction = await client.deleteTransaction(id, options.budget);
|
|
1079
|
+
outputJson({ message: "Transaction deleted", transaction });
|
|
1080
|
+
}
|
|
1081
|
+
)
|
|
1082
|
+
);
|
|
1083
|
+
cmd.command("import").description("Import transactions").option("-b, --budget <id>", "Budget ID").action(
|
|
1084
|
+
withErrorHandling(async (options) => {
|
|
1085
|
+
const transactionIds = await client.importTransactions(options.budget);
|
|
1086
|
+
outputJson({ transaction_ids: transactionIds });
|
|
1087
|
+
})
|
|
1088
|
+
);
|
|
1089
|
+
cmd.command("split").description(
|
|
1090
|
+
"Split transaction into multiple categories. Amounts should be in dollars (e.g., 10.50)."
|
|
1091
|
+
).argument("<id>", "Transaction ID").requiredOption(
|
|
1092
|
+
"--splits <json>",
|
|
1093
|
+
'JSON array of splits with dollar amounts: [{"amount": -21.40, "category_id": "xxx", "memo": "..."}]'
|
|
1094
|
+
).option("-b, --budget <id>", "Budget ID").option("-f, --force", "Force update of already-split transactions by deleting and recreating").action(
|
|
1095
|
+
withErrorHandling(
|
|
1096
|
+
async (id, options) => {
|
|
1097
|
+
let parsedSplits;
|
|
1098
|
+
try {
|
|
1099
|
+
parsedSplits = JSON.parse(options.splits);
|
|
1100
|
+
} catch {
|
|
1101
|
+
throw new YnabCliError("Invalid JSON in --splits parameter", 400);
|
|
1102
|
+
}
|
|
1103
|
+
const splits = validateJson(parsedSplits, TransactionSplitSchema, "transaction splits");
|
|
1104
|
+
const splitsInMilliunits = splits.map((split) => ({
|
|
1105
|
+
...split,
|
|
1106
|
+
amount: amountToMilliunits(split.amount)
|
|
1107
|
+
}));
|
|
1108
|
+
const existingTransaction = await client.getTransaction(id, options.budget);
|
|
1109
|
+
const isAlreadySplit = existingTransaction.subtransactions && existingTransaction.subtransactions.length > 0;
|
|
1110
|
+
if (isAlreadySplit && !options.force) {
|
|
1111
|
+
throw new YnabCliError(
|
|
1112
|
+
"Transaction is already split. YNAB API does not support updating split transactions. Use --force to delete and recreate the transaction with new splits.",
|
|
1113
|
+
400
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (isAlreadySplit) {
|
|
1117
|
+
await client.deleteTransaction(id, options.budget);
|
|
1118
|
+
const recreatedTransaction = await client.createTransaction(
|
|
1119
|
+
{
|
|
1120
|
+
transaction: {
|
|
1121
|
+
account_id: existingTransaction.account_id,
|
|
1122
|
+
date: existingTransaction.date,
|
|
1123
|
+
amount: existingTransaction.amount,
|
|
1124
|
+
payee_id: existingTransaction.payee_id,
|
|
1125
|
+
payee_name: existingTransaction.payee_name,
|
|
1126
|
+
category_id: null,
|
|
1127
|
+
memo: existingTransaction.memo,
|
|
1128
|
+
cleared: existingTransaction.cleared,
|
|
1129
|
+
approved: existingTransaction.approved,
|
|
1130
|
+
flag_color: existingTransaction.flag_color,
|
|
1131
|
+
subtransactions: splitsInMilliunits
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
options.budget
|
|
1135
|
+
);
|
|
1136
|
+
outputJson(recreatedTransaction);
|
|
1137
|
+
} else {
|
|
1138
|
+
const transaction = await client.updateTransaction(
|
|
1139
|
+
id,
|
|
1140
|
+
{
|
|
1141
|
+
transaction: {
|
|
1142
|
+
category_id: null,
|
|
1143
|
+
subtransactions: splitsInMilliunits
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
options.budget
|
|
1147
|
+
);
|
|
1148
|
+
outputJson(transaction);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
1152
|
+
);
|
|
1153
|
+
cmd.command("search").description("Search transactions").option("-b, --budget <id>", "Budget ID").option("--memo <text>", "Search in memo field").option("--payee-name <name>", "Search in payee name").option("--amount <amount>", "Search for exact amount in currency units", parseFloat).option("--since <date>", "Search transactions since date (YYYY-MM-DD)").option("--until <date>", "Search transactions until date (YYYY-MM-DD)").option("--approved <value>", "Filter by approval status: true or false").option(
|
|
1154
|
+
"--status <statuses>",
|
|
1155
|
+
"Filter by cleared status: cleared, uncleared, reconciled (comma-separated)"
|
|
1156
|
+
).option("--fields <fields>", "Comma-separated list of fields to include").action(
|
|
1157
|
+
withErrorHandling(
|
|
1158
|
+
async (options) => {
|
|
1159
|
+
if (!options.memo && !options.payeeName && options.amount === void 0) {
|
|
1160
|
+
throw new YnabCliError(
|
|
1161
|
+
"At least one search criteria required: --memo, --payee-name, or --amount",
|
|
1162
|
+
400
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
const params = {
|
|
1166
|
+
budgetId: options.budget,
|
|
1167
|
+
sinceDate: options.since
|
|
1168
|
+
};
|
|
1169
|
+
const result = await client.getTransactions(params);
|
|
1170
|
+
let transactions = result?.transactions || [];
|
|
1171
|
+
if (options.memo) {
|
|
1172
|
+
const searchTerm = options.memo.toLowerCase();
|
|
1173
|
+
transactions = transactions.filter((t) => t.memo?.toLowerCase().includes(searchTerm));
|
|
1174
|
+
}
|
|
1175
|
+
if (options.payeeName) {
|
|
1176
|
+
const searchTerm = options.payeeName.toLowerCase();
|
|
1177
|
+
transactions = transactions.filter(
|
|
1178
|
+
(t) => t.payee_name?.toLowerCase().includes(searchTerm)
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (options.amount !== void 0) {
|
|
1182
|
+
const amountMilliunits = amountToMilliunits(options.amount);
|
|
1183
|
+
transactions = transactions.filter((t) => t.amount === amountMilliunits);
|
|
1184
|
+
}
|
|
1185
|
+
transactions = applyTransactionFilters(transactions, {
|
|
1186
|
+
until: options.until,
|
|
1187
|
+
approved: options.approved,
|
|
1188
|
+
status: options.status
|
|
1189
|
+
});
|
|
1190
|
+
const filteredTransactions = applyFieldSelection(transactions, options.fields);
|
|
1191
|
+
outputJson(filteredTransactions);
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
);
|
|
1191
1195
|
return cmd;
|
|
1192
1196
|
}
|
|
1193
1197
|
|
|
@@ -1195,24 +1199,37 @@ function createTransactionsCommand() {
|
|
|
1195
1199
|
import { Command as Command7 } from "commander";
|
|
1196
1200
|
function createScheduledCommand() {
|
|
1197
1201
|
const cmd = new Command7("scheduled").description("Scheduled transaction operations");
|
|
1198
|
-
cmd.command("list").description("List all scheduled transactions").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1202
|
+
cmd.command("list").description("List all scheduled transactions").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1203
|
+
withErrorHandling(
|
|
1204
|
+
async (options) => {
|
|
1205
|
+
const result = await client.getScheduledTransactions(
|
|
1206
|
+
options.budget,
|
|
1207
|
+
options.lastKnowledge
|
|
1208
|
+
);
|
|
1209
|
+
outputJson(result?.scheduled_transactions);
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
);
|
|
1213
|
+
cmd.command("view").description("View scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").action(
|
|
1214
|
+
withErrorHandling(async (id, options) => {
|
|
1215
|
+
const scheduledTransaction = await client.getScheduledTransaction(id, options.budget);
|
|
1216
|
+
outputJson(scheduledTransaction);
|
|
1217
|
+
})
|
|
1218
|
+
);
|
|
1219
|
+
cmd.command("delete").description("Delete scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(
|
|
1220
|
+
withErrorHandling(
|
|
1221
|
+
async (id, options) => {
|
|
1222
|
+
if (!await confirmDelete("scheduled transaction", options.yes)) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const scheduledTransaction = await client.deleteScheduledTransaction(id, options.budget);
|
|
1226
|
+
outputJson({
|
|
1227
|
+
message: "Scheduled transaction deleted",
|
|
1228
|
+
scheduled_transaction: scheduledTransaction
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
)
|
|
1232
|
+
);
|
|
1216
1233
|
return cmd;
|
|
1217
1234
|
}
|
|
1218
1235
|
|
|
@@ -1220,38 +1237,54 @@ function createScheduledCommand() {
|
|
|
1220
1237
|
import { Command as Command8 } from "commander";
|
|
1221
1238
|
function createPayeesCommand() {
|
|
1222
1239
|
const cmd = new Command8("payees").description("Payee operations");
|
|
1223
|
-
cmd.command("list").description("List all payees").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
cmd.command("
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1240
|
+
cmd.command("list").description("List all payees").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1241
|
+
withErrorHandling(
|
|
1242
|
+
async (options) => {
|
|
1243
|
+
const result = await client.getPayees(options.budget, options.lastKnowledge);
|
|
1244
|
+
outputJson(result?.payees);
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
);
|
|
1248
|
+
cmd.command("view").description("View payee details").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(
|
|
1249
|
+
withErrorHandling(async (id, options) => {
|
|
1250
|
+
const payee = await client.getPayee(id, options.budget);
|
|
1251
|
+
outputJson(payee);
|
|
1252
|
+
})
|
|
1253
|
+
);
|
|
1254
|
+
cmd.command("update").description("Rename payee").argument("<id>", "Payee ID").requiredOption("--name <name>", "New payee name").option("-b, --budget <id>", "Budget ID").action(
|
|
1255
|
+
withErrorHandling(
|
|
1256
|
+
async (id, options) => {
|
|
1257
|
+
if (!options.name?.trim()) {
|
|
1258
|
+
throw new YnabCliError("Name cannot be empty", 400);
|
|
1259
|
+
}
|
|
1260
|
+
const payee = await client.updatePayee(
|
|
1261
|
+
id,
|
|
1262
|
+
{ payee: { name: options.name } },
|
|
1263
|
+
options.budget
|
|
1264
|
+
);
|
|
1265
|
+
outputJson(payee);
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
1269
|
+
cmd.command("locations").description("List locations for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(
|
|
1270
|
+
withErrorHandling(async (id, options) => {
|
|
1271
|
+
const locations = await client.getPayeeLocationsByPayee(id, options.budget);
|
|
1272
|
+
outputJson(locations);
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
cmd.command("transactions").description("List transactions for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").option("--since <date>", "Filter transactions since date (YYYY-MM-DD)").option("--type <type>", "Filter by transaction type").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1276
|
+
withErrorHandling(
|
|
1277
|
+
async (id, options) => {
|
|
1278
|
+
const result = await client.getTransactionsByPayee(id, {
|
|
1279
|
+
budgetId: options.budget,
|
|
1280
|
+
sinceDate: options.since,
|
|
1281
|
+
type: options.type,
|
|
1282
|
+
lastKnowledgeOfServer: options.lastKnowledge
|
|
1283
|
+
});
|
|
1284
|
+
outputJson(result?.transactions);
|
|
1285
|
+
}
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1255
1288
|
return cmd;
|
|
1256
1289
|
}
|
|
1257
1290
|
|
|
@@ -1259,14 +1292,20 @@ function createPayeesCommand() {
|
|
|
1259
1292
|
import { Command as Command9 } from "commander";
|
|
1260
1293
|
function createMonthsCommand() {
|
|
1261
1294
|
const cmd = new Command9("months").description("Monthly budget operations");
|
|
1262
|
-
cmd.command("list").description("List all budget months").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1295
|
+
cmd.command("list").description("List all budget months").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(
|
|
1296
|
+
withErrorHandling(
|
|
1297
|
+
async (options) => {
|
|
1298
|
+
const result = await client.getBudgetMonths(options.budget, options.lastKnowledge);
|
|
1299
|
+
outputJson(result?.months);
|
|
1300
|
+
}
|
|
1301
|
+
)
|
|
1302
|
+
);
|
|
1303
|
+
cmd.command("view").description("View specific month details").argument("<month>", "Month in YYYY-MM-DD format (e.g., 2025-07-01)").option("-b, --budget <id>", "Budget ID").action(
|
|
1304
|
+
withErrorHandling(async (month, options) => {
|
|
1305
|
+
const monthData = await client.getBudgetMonth(month, options.budget);
|
|
1306
|
+
outputJson(monthData);
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1270
1309
|
return cmd;
|
|
1271
1310
|
}
|
|
1272
1311
|
|
|
@@ -1275,32 +1314,36 @@ import { Command as Command10 } from "commander";
|
|
|
1275
1314
|
var VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
1276
1315
|
function createApiCommand() {
|
|
1277
1316
|
const cmd = new Command10("api").description("Raw API access");
|
|
1278
|
-
cmd.argument("<method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").argument("<path>", "API path (e.g., /budgets or /budgets/{budget_id}/transactions)").option("-b, --budget <id>", "Budget ID (used to replace {budget_id} in path)").option("--data <json>", "JSON data for POST/PUT/PATCH requests").description("Make raw API calls to YNAB").action(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1317
|
+
cmd.argument("<method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").argument("<path>", "API path (e.g., /budgets or /budgets/{budget_id}/transactions)").option("-b, --budget <id>", "Budget ID (used to replace {budget_id} in path)").option("--data <json>", "JSON data for POST/PUT/PATCH requests").description("Make raw API calls to YNAB").action(
|
|
1318
|
+
withErrorHandling(
|
|
1319
|
+
async (method, path, options) => {
|
|
1320
|
+
const upperMethod = method.toUpperCase();
|
|
1321
|
+
if (!VALID_HTTP_METHODS.includes(upperMethod)) {
|
|
1322
|
+
throw new YnabCliError(
|
|
1323
|
+
`Invalid HTTP method: ${method}. Must be one of: ${VALID_HTTP_METHODS.join(", ")}`,
|
|
1324
|
+
400
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
let data;
|
|
1328
|
+
if (options.data) {
|
|
1329
|
+
try {
|
|
1330
|
+
const parsedData = JSON.parse(options.data);
|
|
1331
|
+
data = validateJson(parsedData, ApiDataSchema, "API data");
|
|
1332
|
+
} catch {
|
|
1333
|
+
throw new YnabCliError("Invalid JSON in --data parameter", 400);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const result = await client.rawApiCall(upperMethod, path, data, options.budget);
|
|
1337
|
+
outputJson(result);
|
|
1293
1338
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
outputJson(result);
|
|
1297
|
-
}));
|
|
1339
|
+
)
|
|
1340
|
+
);
|
|
1298
1341
|
return cmd;
|
|
1299
1342
|
}
|
|
1300
1343
|
|
|
1301
1344
|
// src/cli.ts
|
|
1302
1345
|
var program = new Command11();
|
|
1303
|
-
program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("1.2.
|
|
1346
|
+
program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("1.2.6").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
|
|
1304
1347
|
const options = thisCommand.opts();
|
|
1305
1348
|
setOutputOptions({
|
|
1306
1349
|
compact: options.compact
|