@stephendolan/ynab-cli 1.2.4 → 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 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(`Invalid status '${status}'. Must be one of: ${validStatuses.join(", ")}`, 400);
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
- fieldList.forEach((field) => {
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();
@@ -784,8 +704,9 @@ var YnabClient = class {
784
704
  await this.getApi();
785
705
  const fullPath = path.includes("{budget_id}") ? path.replace("{budget_id}", await this.getBudgetId(budgetId)) : path;
786
706
  const url = `https://api.ynab.com/v1${fullPath}`;
707
+ const accessToken = await auth.getAccessToken() || process.env.YNAB_API_KEY;
787
708
  const headers = {
788
- Authorization: `Bearer ${await auth.getAccessToken()}`,
709
+ Authorization: `Bearer ${accessToken}`,
789
710
  "Content-Type": "application/json"
790
711
  };
791
712
  const httpMethod = method.toUpperCase();
@@ -845,40 +766,46 @@ function buildUpdateObject(options, mapping) {
845
766
  // src/commands/auth.ts
846
767
  function createAuthCommand() {
847
768
  const cmd = new Command("auth").description("Authentication management");
848
- cmd.command("login").description("Configure access token").action(withErrorHandling(async () => {
849
- const token = await promptForAccessToken();
850
- await auth.setAccessToken(token);
851
- client.clearApi();
852
- try {
853
- const user = await client.getUser();
854
- outputJson({
855
- message: "Successfully authenticated",
856
- user: { id: user?.id }
857
- });
858
- } catch (error) {
859
- 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);
860
773
  client.clearApi();
861
- throw error;
862
- }
863
- }));
864
- cmd.command("status").description("Check authentication status").action(withErrorHandling(async () => {
865
- const isAuthenticated = await auth.isAuthenticated();
866
- if (!isAuthenticated) {
867
- outputJson({ authenticated: false, message: "Not authenticated" });
868
- return;
869
- }
870
- try {
871
- const user = await client.getUser();
872
- outputJson({ authenticated: true, user: { id: user?.id } });
873
- } catch (error) {
874
- outputJson({ authenticated: false, message: "Token exists but is invalid" });
875
- }
876
- }));
877
- cmd.command("logout").description("Remove stored credentials").action(withErrorHandling(async () => {
878
- await auth.logout();
879
- client.clearApi();
880
- outputJson({ message: "Successfully logged out" });
881
- }));
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
+ );
882
809
  return cmd;
883
810
  }
884
811
 
@@ -886,10 +813,12 @@ function createAuthCommand() {
886
813
  import { Command as Command2 } from "commander";
887
814
  function createUserCommand() {
888
815
  const cmd = new Command2("user").description("User information");
889
- cmd.command("info").description("Get authenticated user information").action(withErrorHandling(async () => {
890
- const user = await client.getUser();
891
- outputJson(user);
892
- }));
816
+ cmd.command("info").description("Get authenticated user information").action(
817
+ withErrorHandling(async () => {
818
+ const user = await client.getUser();
819
+ outputJson(user);
820
+ })
821
+ );
893
822
  return cmd;
894
823
  }
895
824
 
@@ -897,30 +826,38 @@ function createUserCommand() {
897
826
  import { Command as Command3 } from "commander";
898
827
  function createBudgetsCommand() {
899
828
  const cmd = new Command3("budgets").description("Budget operations");
900
- cmd.command("list").description("List all budgets").option("--include-accounts", "Include accounts in response").action(withErrorHandling(async (options) => {
901
- const result = await client.getBudgets(options.includeAccounts);
902
- outputJson(result?.budgets);
903
- }));
904
- cmd.command("view").description("View budget details (uses default if no id provided)").argument("[id]", "Budget ID").action(withErrorHandling(async (id) => {
905
- const result = await client.getBudget(id);
906
- outputJson(result?.budget);
907
- }));
908
- cmd.command("settings").description("View budget settings").argument("[id]", "Budget ID").action(withErrorHandling(async (id) => {
909
- const settings = await client.getBudgetSettings(id);
910
- outputJson(settings);
911
- }));
912
- cmd.command("set-default").description("Set default budget for commands").argument("<id>", "Budget ID").action(withErrorHandling(async (id) => {
913
- const result = await client.getBudgets();
914
- const budget = result?.budgets.find((b) => b.id === id);
915
- if (!budget) {
916
- throw new YnabCliError(`Budget with ID ${id} not found`, 404);
917
- }
918
- config.setDefaultBudget(id);
919
- outputJson({
920
- message: "Default budget set",
921
- budget: { id: budget.id, name: budget.name }
922
- });
923
- }));
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
+ );
924
861
  return cmd;
925
862
  }
926
863
 
@@ -928,22 +865,30 @@ function createBudgetsCommand() {
928
865
  import { Command as Command4 } from "commander";
929
866
  function createAccountsCommand() {
930
867
  const cmd = new Command4("accounts").description("Account operations");
931
- cmd.command("list").description("List all accounts").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (options) => {
932
- const result = await client.getAccounts(options.budget);
933
- outputJson(result?.accounts);
934
- }));
935
- cmd.command("view").description("View account details").argument("<id>", "Account ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
936
- const account = await client.getAccount(id, options.budget);
937
- outputJson(account);
938
- }));
939
- 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(withErrorHandling(async (id, options) => {
940
- const result = await client.getTransactionsByAccount(id, {
941
- budgetId: options.budget,
942
- sinceDate: options.since,
943
- type: options.type
944
- });
945
- outputJson(result?.transactions);
946
- }));
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
+ );
947
892
  return cmd;
948
893
  }
949
894
 
@@ -951,36 +896,50 @@ function createAccountsCommand() {
951
896
  import { Command as Command5 } from "commander";
952
897
  function createCategoriesCommand() {
953
898
  const cmd = new Command5("categories").description("Category operations");
954
- cmd.command("list").description("List all categories").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
955
- const result = await client.getCategories(options.budget, options.lastKnowledge);
956
- outputJson(result?.category_groups);
957
- }));
958
- cmd.command("view").description("View category details").argument("<id>", "Category ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
959
- const category = await client.getCategory(id, options.budget);
960
- outputJson(category);
961
- }));
962
- 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(withErrorHandling(async (id, options) => {
963
- if (isNaN(options.amount)) {
964
- throw new YnabCliError("Amount must be a valid number", 400);
965
- }
966
- const milliunits = amountToMilliunits(options.amount);
967
- const category = await client.updateMonthCategory(
968
- options.month,
969
- id,
970
- { category: { budgeted: milliunits } },
971
- options.budget
972
- );
973
- outputJson(category);
974
- }));
975
- 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(withErrorHandling(async (id, options) => {
976
- const result = await client.getTransactionsByCategory(id, {
977
- budgetId: options.budget,
978
- sinceDate: options.since,
979
- type: options.type,
980
- lastKnowledgeOfServer: options.lastKnowledge
981
- });
982
- outputJson(result?.transactions);
983
- }));
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
+ );
984
943
  return cmd;
985
944
  }
986
945
 
@@ -1032,161 +991,207 @@ function buildTransactionData(options) {
1032
991
  }
1033
992
  function createTransactionsCommand() {
1034
993
  const cmd = new Command6("transactions").description("Transaction operations");
1035
- 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("--status <statuses>", "Filter by cleared status: cleared, uncleared, reconciled (comma-separated for multiple)").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("--fields <fields>", "Comma-separated list of fields to include (e.g., id,date,amount,memo)").action(withErrorHandling(async (options) => {
1036
- const params = {
1037
- budgetId: options.budget,
1038
- sinceDate: options.since,
1039
- type: options.type
1040
- };
1041
- 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);
1042
- const transactions = result?.transactions || [];
1043
- const filtered = applyTransactionFilters(transactions, {
1044
- until: options.until,
1045
- approved: options.approved,
1046
- status: options.status,
1047
- minAmount: options.minAmount,
1048
- maxAmount: options.maxAmount
1049
- });
1050
- const selected = applyFieldSelection(filtered, options.fields);
1051
- outputJson(selected);
1052
- }));
1053
- cmd.command("view").description("View single transaction").argument("<id>", "Transaction ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1054
- const transaction = await client.getTransaction(id, options.budget);
1055
- outputJson(transaction);
1056
- }));
1057
- 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(withErrorHandling(async (options) => {
1058
- const shouldPrompt = isInteractive() && !options.amount;
1059
- if (shouldPrompt && !options.account) {
1060
- throw new YnabCliError("--account is required. Interactive mode cannot auto-select an account.", 400);
1061
- }
1062
- const transactionData = shouldPrompt ? { ...await promptForTransaction(), account_id: options.account } : buildTransactionData(options);
1063
- const transaction = await client.createTransaction(
1064
- { transaction: transactionData },
1065
- options.budget
1066
- );
1067
- outputJson(transaction);
1068
- }));
1069
- 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) => {
1070
- const transactionData = buildUpdateObject(options, {
1071
- account: "account_id",
1072
- date: "date",
1073
- payeeName: "payee_name",
1074
- payeeId: "payee_id",
1075
- categoryId: "category_id",
1076
- memo: "memo",
1077
- cleared: "cleared",
1078
- approved: "approved"
1079
- });
1080
- if (options.amount !== void 0) {
1081
- transactionData.amount = amountToMilliunits(options.amount);
1082
- }
1083
- const transaction = await client.updateTransaction(
1084
- id,
1085
- { transaction: transactionData },
1086
- options.budget
1087
- );
1088
- outputJson(transaction);
1089
- }));
1090
- 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) => {
1091
- if (!await confirmDelete("transaction", options.yes)) {
1092
- return;
1093
- }
1094
- const transaction = await client.deleteTransaction(id, options.budget);
1095
- outputJson({ message: "Transaction deleted", transaction });
1096
- }));
1097
- cmd.command("import").description("Import transactions").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (options) => {
1098
- const transactionIds = await client.importTransactions(options.budget);
1099
- outputJson({ transaction_ids: transactionIds });
1100
- }));
1101
- 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) => {
1102
- let parsedSplits;
1103
- try {
1104
- parsedSplits = JSON.parse(options.splits);
1105
- } catch (error) {
1106
- throw new YnabCliError("Invalid JSON in --splits parameter", 400);
1107
- }
1108
- const splits = validateJson(parsedSplits, TransactionSplitSchema, "transaction splits");
1109
- const splitsInMilliunits = splits.map((split) => ({
1110
- ...split,
1111
- amount: amountToMilliunits(split.amount)
1112
- }));
1113
- const existingTransaction = await client.getTransaction(id, options.budget);
1114
- const isAlreadySplit = existingTransaction.subtransactions && existingTransaction.subtransactions.length > 0;
1115
- if (isAlreadySplit && !options.force) {
1116
- throw new YnabCliError(
1117
- "Transaction is already split. YNAB API does not support updating split transactions. Use --force to delete and recreate the transaction with new splits.",
1118
- 400
1119
- );
1120
- }
1121
- if (isAlreadySplit) {
1122
- await client.deleteTransaction(id, options.budget);
1123
- const recreatedTransaction = await client.createTransaction(
1124
- {
1125
- transaction: {
1126
- account_id: existingTransaction.account_id,
1127
- date: existingTransaction.date,
1128
- amount: existingTransaction.amount,
1129
- payee_id: existingTransaction.payee_id,
1130
- payee_name: existingTransaction.payee_name,
1131
- category_id: null,
1132
- memo: existingTransaction.memo,
1133
- cleared: existingTransaction.cleared,
1134
- approved: existingTransaction.approved,
1135
- flag_color: existingTransaction.flag_color,
1136
- subtransactions: splitsInMilliunits
1137
- }
1138
- },
1139
- options.budget
1140
- );
1141
- outputJson(recreatedTransaction);
1142
- } else {
1143
- const transaction = await client.updateTransaction(
1144
- id,
1145
- {
1146
- transaction: {
1147
- category_id: null,
1148
- subtransactions: splitsInMilliunits
1149
- }
1150
- },
1151
- options.budget
1152
- );
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);
1153
1025
  outputJson(transaction);
1154
- }
1155
- }));
1156
- 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("--status <statuses>", "Filter by cleared status: cleared, uncleared, reconciled (comma-separated)").option("--fields <fields>", "Comma-separated list of fields to include").action(withErrorHandling(async (options) => {
1157
- if (!options.memo && !options.payeeName && options.amount === void 0) {
1158
- throw new YnabCliError("At least one search criteria required: --memo, --payee-name, or --amount", 400);
1159
- }
1160
- const params = {
1161
- budgetId: options.budget,
1162
- sinceDate: options.since
1163
- };
1164
- const result = await client.getTransactions(params);
1165
- let transactions = result?.transactions || [];
1166
- if (options.memo) {
1167
- const searchTerm = options.memo.toLowerCase();
1168
- transactions = transactions.filter(
1169
- (t) => t.memo?.toLowerCase().includes(searchTerm)
1170
- );
1171
- }
1172
- if (options.payeeName) {
1173
- const searchTerm = options.payeeName.toLowerCase();
1174
- transactions = transactions.filter(
1175
- (t) => t.payee_name?.toLowerCase().includes(searchTerm)
1176
- );
1177
- }
1178
- if (options.amount !== void 0) {
1179
- const amountMilliunits = amountToMilliunits(options.amount);
1180
- transactions = transactions.filter((t) => t.amount === amountMilliunits);
1181
- }
1182
- transactions = applyTransactionFilters(transactions, {
1183
- until: options.until,
1184
- approved: options.approved,
1185
- status: options.status
1186
- });
1187
- const filteredTransactions = applyFieldSelection(transactions, options.fields);
1188
- outputJson(filteredTransactions);
1189
- }));
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
+ );
1190
1195
  return cmd;
1191
1196
  }
1192
1197
 
@@ -1194,24 +1199,37 @@ function createTransactionsCommand() {
1194
1199
  import { Command as Command7 } from "commander";
1195
1200
  function createScheduledCommand() {
1196
1201
  const cmd = new Command7("scheduled").description("Scheduled transaction operations");
1197
- cmd.command("list").description("List all scheduled transactions").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1198
- const result = await client.getScheduledTransactions(options.budget, options.lastKnowledge);
1199
- outputJson(result?.scheduled_transactions);
1200
- }));
1201
- cmd.command("view").description("View scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1202
- const scheduledTransaction = await client.getScheduledTransaction(id, options.budget);
1203
- outputJson(scheduledTransaction);
1204
- }));
1205
- cmd.command("delete").description("Delete scheduled transaction").argument("<id>", "Scheduled transaction ID").option("-b, --budget <id>", "Budget ID").option("-y, --yes", "Skip confirmation").action(withErrorHandling(async (id, options) => {
1206
- if (!await confirmDelete("scheduled transaction", options.yes)) {
1207
- return;
1208
- }
1209
- const scheduledTransaction = await client.deleteScheduledTransaction(id, options.budget);
1210
- outputJson({
1211
- message: "Scheduled transaction deleted",
1212
- scheduled_transaction: scheduledTransaction
1213
- });
1214
- }));
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
+ );
1215
1233
  return cmd;
1216
1234
  }
1217
1235
 
@@ -1219,38 +1237,54 @@ function createScheduledCommand() {
1219
1237
  import { Command as Command8 } from "commander";
1220
1238
  function createPayeesCommand() {
1221
1239
  const cmd = new Command8("payees").description("Payee operations");
1222
- cmd.command("list").description("List all payees").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1223
- const result = await client.getPayees(options.budget, options.lastKnowledge);
1224
- outputJson(result?.payees);
1225
- }));
1226
- cmd.command("view").description("View payee details").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1227
- const payee = await client.getPayee(id, options.budget);
1228
- outputJson(payee);
1229
- }));
1230
- cmd.command("update").description("Rename payee").argument("<id>", "Payee ID").requiredOption("--name <name>", "New payee name").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1231
- if (!options.name?.trim()) {
1232
- throw new YnabCliError("Name cannot be empty", 400);
1233
- }
1234
- const payee = await client.updatePayee(
1235
- id,
1236
- { payee: { name: options.name } },
1237
- options.budget
1238
- );
1239
- outputJson(payee);
1240
- }));
1241
- cmd.command("locations").description("List locations for payee").argument("<id>", "Payee ID").option("-b, --budget <id>", "Budget ID").action(withErrorHandling(async (id, options) => {
1242
- const locations = await client.getPayeeLocationsByPayee(id, options.budget);
1243
- outputJson(locations);
1244
- }));
1245
- 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(withErrorHandling(async (id, options) => {
1246
- const result = await client.getTransactionsByPayee(id, {
1247
- budgetId: options.budget,
1248
- sinceDate: options.since,
1249
- type: options.type,
1250
- lastKnowledgeOfServer: options.lastKnowledge
1251
- });
1252
- outputJson(result?.transactions);
1253
- }));
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
+ );
1254
1288
  return cmd;
1255
1289
  }
1256
1290
 
@@ -1258,14 +1292,20 @@ function createPayeesCommand() {
1258
1292
  import { Command as Command9 } from "commander";
1259
1293
  function createMonthsCommand() {
1260
1294
  const cmd = new Command9("months").description("Monthly budget operations");
1261
- cmd.command("list").description("List all budget months").option("-b, --budget <id>", "Budget ID").option("--last-knowledge <number>", "Last knowledge of server", parseInt).action(withErrorHandling(async (options) => {
1262
- const result = await client.getBudgetMonths(options.budget, options.lastKnowledge);
1263
- outputJson(result?.months);
1264
- }));
1265
- 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(withErrorHandling(async (month, options) => {
1266
- const monthData = await client.getBudgetMonth(month, options.budget);
1267
- outputJson(monthData);
1268
- }));
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
+ );
1269
1309
  return cmd;
1270
1310
  }
1271
1311
 
@@ -1274,32 +1314,36 @@ import { Command as Command10 } from "commander";
1274
1314
  var VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
1275
1315
  function createApiCommand() {
1276
1316
  const cmd = new Command10("api").description("Raw API access");
1277
- 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(withErrorHandling(async (method, path, options) => {
1278
- const upperMethod = method.toUpperCase();
1279
- if (!VALID_HTTP_METHODS.includes(upperMethod)) {
1280
- throw new YnabCliError(
1281
- `Invalid HTTP method: ${method}. Must be one of: ${VALID_HTTP_METHODS.join(", ")}`,
1282
- 400
1283
- );
1284
- }
1285
- let data;
1286
- if (options.data) {
1287
- try {
1288
- const parsedData = JSON.parse(options.data);
1289
- data = validateJson(parsedData, ApiDataSchema, "API data");
1290
- } catch (error) {
1291
- throw new YnabCliError("Invalid JSON in --data parameter", 400);
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);
1292
1338
  }
1293
- }
1294
- const result = await client.rawApiCall(upperMethod, path, data, options.budget);
1295
- outputJson(result);
1296
- }));
1339
+ )
1340
+ );
1297
1341
  return cmd;
1298
1342
  }
1299
1343
 
1300
1344
  // src/cli.ts
1301
1345
  var program = new Command11();
1302
- program.name("ynab").description("A command-line interface for You Need a Budget (YNAB)").version("1.2.4").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
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) => {
1303
1347
  const options = thisCommand.opts();
1304
1348
  setOutputOptions({
1305
1349
  compact: options.compact