From 383cabffee2e4399e54269fe48ff962595d646cc Mon Sep 17 00:00:00 2001 From: Juan Carlos Orrego Date: Wed, 28 Jan 2026 19:50:40 +0100 Subject: [PATCH 1/2] feat: add category update command - Add updateCategory method to API client - Add 'categories update' command with options for name, note, category group, and goal target - Update README with new command documentation - Support updating category name, note, moving to different group, and modifying goal target - Amounts are specified in dollars and converted to milliunits automatically --- README.md | 1 + src/commands/categories.ts | 60 ++++++++++++++++++++++++++++++++++++++ src/lib/api-client.ts | 9 ++++++ 3 files changed, 70 insertions(+) diff --git a/README.md b/README.md index b8f0bad..481fd4a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ ynab accounts transactions ```bash ynab categories list ynab categories view +ynab categories update --name --note --category-group-id --goal-target ynab categories budget --month --amount ynab categories transactions ``` diff --git a/src/commands/categories.ts b/src/commands/categories.ts index f8ba596..7383bf2 100644 --- a/src/commands/categories.ts +++ b/src/commands/categories.ts @@ -36,6 +36,66 @@ export function createCategoriesCommand(): Command { }) ); + cmd + .command('update') + .description('Update category details') + .argument('', 'Category ID') + .option('--name ', 'New category name') + .option('--note ', 'New category note') + .option('--category-group-id ', 'Move to a different category group') + .option('--goal-target ', 'Goal target amount (only if goal already exists)', parseFloat) + .option('-b, --budget ', 'Budget ID') + .action( + withErrorHandling( + async ( + id: string, + options: { + name?: string; + note?: string; + categoryGroupId?: string; + goalTarget?: number; + budget?: string; + } & CommandOptions + ) => { + // Validate at least one field is provided + if (!options.name && !options.note && !options.categoryGroupId && options.goalTarget === undefined) { + throw new YnabCliError( + 'At least one field to update must be provided (--name, --note, --category-group-id, or --goal-target)', + 400 + ); + } + + // Validate goal-target if provided + if (options.goalTarget !== undefined && isNaN(options.goalTarget)) { + throw new YnabCliError('Goal target must be a valid number', 400); + } + + const updateData: { + name?: string | null; + note?: string | null; + category_group_id?: string; + goal_target?: number | null; + } = {}; + + if (options.name !== undefined) { + updateData.name = options.name.trim() || null; + } + if (options.note !== undefined) { + updateData.note = options.note.trim() || null; + } + if (options.categoryGroupId) { + updateData.category_group_id = options.categoryGroupId; + } + if (options.goalTarget !== undefined) { + updateData.goal_target = amountToMilliunits(options.goalTarget); + } + + const category = await client.updateCategory(id, { category: updateData }, options.budget); + outputJson(category); + } + ) + ); + cmd .command('budget') .description('Set category budgeted amount for a month (overrides existing amount)') diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index e95ff12..f6bf6f8 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -159,6 +159,15 @@ export class YnabClient { }); } + async updateCategory(categoryId: string, data: ynab.PatchCategoryWrapper, budgetId?: string) { + return this.withErrorHandling(async () => { + const api = await this.getApi(); + const id = await this.getBudgetId(budgetId); + const response = await api.categories.updateCategory(id, categoryId, data); + return response.data.category; + }); + } + async getPayees(budgetId?: string, lastKnowledgeOfServer?: number) { return this.withErrorHandling(async () => { const api = await this.getApi(); From a578d133c121a5db27a27851d11b60b0d488d4c7 Mon Sep 17 00:00:00 2001 From: Juan Carlos Orrego Date: Wed, 28 Jan 2026 20:42:33 +0100 Subject: [PATCH 2/2] Address Copilot review comments - Fix validation to use !== undefined checks instead of falsy checks - Reject empty/whitespace category names instead of converting to null - Update README to show optional flags with brackets --- README.md | 2 +- src/commands/categories.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 481fd4a..c703552 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ ynab accounts transactions ```bash ynab categories list ynab categories view -ynab categories update --name --note --category-group-id --goal-target +ynab categories update [--name ] [--note ] [--category-group-id ] [--goal-target ] ynab categories budget --month --amount ynab categories transactions ``` diff --git a/src/commands/categories.ts b/src/commands/categories.ts index 7383bf2..feea070 100644 --- a/src/commands/categories.ts +++ b/src/commands/categories.ts @@ -58,27 +58,32 @@ export function createCategoriesCommand(): Command { } & CommandOptions ) => { // Validate at least one field is provided - if (!options.name && !options.note && !options.categoryGroupId && options.goalTarget === undefined) { + if (options.name === undefined && options.note === undefined && options.categoryGroupId === undefined && options.goalTarget === undefined) { throw new YnabCliError( 'At least one field to update must be provided (--name, --note, --category-group-id, or --goal-target)', 400 ); } + // Validate name if provided + if (options.name !== undefined && options.name.trim() === '') { + throw new YnabCliError('Category name cannot be empty or whitespace', 400); + } + // Validate goal-target if provided if (options.goalTarget !== undefined && isNaN(options.goalTarget)) { throw new YnabCliError('Goal target must be a valid number', 400); } const updateData: { - name?: string | null; + name?: string; note?: string | null; category_group_id?: string; goal_target?: number | null; } = {}; if (options.name !== undefined) { - updateData.name = options.name.trim() || null; + updateData.name = options.name.trim(); } if (options.note !== undefined) { updateData.note = options.note.trim() || null;