From 91b78a6ae6c020d9d35ba3f46a9516380788cb01 Mon Sep 17 00:00:00 2001 From: Stephen Dolan Date: Sat, 27 Dec 2025 20:03:00 -0500 Subject: [PATCH] chore(release): 2.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove chalk, date-fns, inquirer, and zod dependencies - Add dayjs for flexible date parsing - Replace Zod validation with manual validation - Remove interactive prompt functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- bun.lock | 114 +--------------------------- package.json | 10 +-- src/commands/accounts.ts | 5 +- src/commands/api.ts | 7 +- src/commands/auth.ts | 10 ++- src/commands/categories.ts | 9 ++- src/commands/months.ts | 5 +- src/commands/payees.ts | 5 +- src/commands/scheduled.ts | 7 +- src/commands/transactions.ts | 48 +++++------- src/lib/command-utils.ts | 26 ++----- src/lib/dates.ts | 14 ++++ src/lib/prompts.ts | 60 --------------- src/lib/schemas.test.ts | 139 ----------------------------------- src/lib/schemas.ts | 51 ++++++++----- src/lib/utils.ts | 4 - 16 files changed, 102 insertions(+), 412 deletions(-) create mode 100644 src/lib/dates.ts delete mode 100644 src/lib/prompts.ts delete mode 100644 src/lib/schemas.test.ts diff --git a/bun.lock b/bun.lock index b9d8993..440c106 100644 --- a/bun.lock +++ b/bun.lock @@ -6,18 +6,14 @@ "name": "@stephendolan/ynab-cli", "dependencies": { "@napi-rs/keyring": "^1.2.0", - "chalk": "^5.3.0", "commander": "^12.0.0", "conf": "^12.0.0", - "date-fns": "^3.0.0", + "dayjs": "^1.11.19", "dotenv": "^16.4.0", - "inquirer": "^9.2.0", "ynab": "^2.10.0", - "zod": "^3.22.0", }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/inquirer": "^9.0.0", "@types/node": "^20.0.0", "oxlint": "^0.16.0", "tsup": "^8.0.0", @@ -98,10 +94,6 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -204,12 +196,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/inquirer": ["@types/inquirer@9.0.9", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw=="], - "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], - "@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="], - "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="], @@ -230,48 +218,20 @@ "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "conf": ["conf@12.0.0", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "atomically": "^2.0.2", "debounce-fn": "^5.1.2", "dot-prop": "^8.0.2", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.5.4", "uint8array-extras": "^0.3.0" } }, "sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA=="], @@ -280,20 +240,16 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], "debounce-fn": ["debounce-fn@5.1.2", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], @@ -318,22 +274,6 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "inquirer": ["inquirer@9.3.8", "", { "dependencies": { "@inquirer/external-editor": "^1.0.2", "@inquirer/figures": "^1.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "1.0.0", "ora": "^5.4.1", "run-async": "^3.0.0", "rxjs": "^7.8.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -346,8 +286,6 @@ "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], @@ -356,8 +294,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -368,10 +304,6 @@ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "oxlint": ["oxlint@0.16.12", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.16.12", "@oxlint/darwin-x64": "0.16.12", "@oxlint/linux-arm64-gnu": "0.16.12", "@oxlint/linux-arm64-musl": "0.16.12", "@oxlint/linux-x64-gnu": "0.16.12", "@oxlint/linux-x64-musl": "0.16.12", "@oxlint/win32-arm64": "0.16.12", "@oxlint/win32-x64": "0.16.12" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-1oN3P9bzE90zkbjLTc+uICVLwSR+eQaDaYVipS0BtmtmEd3ccQue0y7npCinb35YqKzIv1LZxhoU9nm5fgmQuw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -388,8 +320,6 @@ "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -398,24 +328,12 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], - "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], - - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -424,20 +342,12 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], @@ -456,8 +366,6 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], @@ -472,14 +380,10 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -488,22 +392,8 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "ynab": ["ynab@2.10.0", "", { "dependencies": { "fetch-ponyfill": "^7.1.0" } }, "sha512-zDH++4mbFpVDbDW1qIYS3pG3sPVtdth7c45aEfU7pVNqIcK6aVzK8eSksyOarrzaPKSq4OA9AXq1ixC7WGdpow=="], - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], diff --git a/package.json b/package.json index 9fc46a8..92562b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stephendolan/ynab-cli", - "version": "2.0.0", + "version": "2.1.0", "description": "A command-line interface for You Need a Budget (YNAB)", "type": "module", "main": "./dist/cli.js", @@ -46,18 +46,14 @@ ], "dependencies": { "@napi-rs/keyring": "^1.2.0", - "chalk": "^5.3.0", "commander": "^12.0.0", "conf": "^12.0.0", - "date-fns": "^3.0.0", + "dayjs": "^1.11.19", "dotenv": "^16.4.0", - "inquirer": "^9.2.0", - "ynab": "^2.10.0", - "zod": "^3.22.0" + "ynab": "^2.10.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/inquirer": "^9.0.0", "@types/node": "^20.0.0", "oxlint": "^0.16.0", "tsup": "^8.0.0", diff --git a/src/commands/accounts.ts b/src/commands/accounts.ts index 7e2b118..e3f5f69 100644 --- a/src/commands/accounts.ts +++ b/src/commands/accounts.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; import { withErrorHandling } from '../lib/command-utils.js'; +import { parseDate } from '../lib/dates.js'; import type { CommandOptions } from '../types/index.js'; export function createAccountsCommand(): Command { @@ -35,7 +36,7 @@ export function createAccountsCommand(): Command { .description('List transactions for account') .argument('', 'Account ID') .option('-b, --budget ', 'Budget ID') - .option('--since ', 'Filter transactions since date (YYYY-MM-DD)') + .option('--since ', 'Filter transactions since date') .option('--type ', 'Filter by transaction type') .action( withErrorHandling( @@ -49,7 +50,7 @@ export function createAccountsCommand(): Command { ) => { const result = await client.getTransactionsByAccount(id, { budgetId: options.budget, - sinceDate: options.since, + sinceDate: options.since ? parseDate(options.since) : undefined, type: options.type, }); outputJson(result?.transactions); diff --git a/src/commands/api.ts b/src/commands/api.ts index bd59750..cc6370f 100644 --- a/src/commands/api.ts +++ b/src/commands/api.ts @@ -3,7 +3,7 @@ import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; import { YnabCliError } from '../lib/errors.js'; import { withErrorHandling } from '../lib/command-utils.js'; -import { validateJson, ApiDataSchema } from '../lib/schemas.js'; +import { validateApiData } from '../lib/schemas.js'; import type { CommandOptions } from '../types/index.js'; const VALID_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; @@ -35,12 +35,13 @@ export function createApiCommand(): Command { let data: Record | undefined; if (options.data) { + let parsedData: unknown; try { - const parsedData = JSON.parse(options.data); - data = validateJson(parsedData, ApiDataSchema, 'API data'); + parsedData = JSON.parse(options.data); } catch { throw new YnabCliError('Invalid JSON in --data parameter', 400); } + data = validateApiData(parsedData); } const result = await client.rawApiCall(upperMethod, path, data, options.budget); diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 1d578fb..7c7d907 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -1,9 +1,9 @@ import { Command } from 'commander'; import { auth } from '../lib/auth.js'; -import { promptForAccessToken } from '../lib/prompts.js'; import { outputJson } from '../lib/output.js'; import { client } from '../lib/api-client.js'; import { withErrorHandling } from '../lib/command-utils.js'; +import { YnabCliError } from '../lib/errors.js'; export function createAuthCommand(): Command { const cmd = new Command('auth').description('Authentication management'); @@ -11,9 +11,13 @@ export function createAuthCommand(): Command { cmd .command('login') .description('Configure access token') + .requiredOption('-t, --token ', 'YNAB Personal Access Token') .action( - withErrorHandling(async () => { - const token = await promptForAccessToken(); + withErrorHandling(async (options: { token: string }) => { + const token = options.token.trim(); + if (!token) { + throw new YnabCliError('Access token cannot be empty', 400); + } await auth.setAccessToken(token); client.clearApi(); diff --git a/src/commands/categories.ts b/src/commands/categories.ts index 242b013..f8ba596 100644 --- a/src/commands/categories.ts +++ b/src/commands/categories.ts @@ -4,6 +4,7 @@ import { outputJson } from '../lib/output.js'; import { YnabCliError } from '../lib/errors.js'; import { amountToMilliunits } from '../lib/utils.js'; import { withErrorHandling } from '../lib/command-utils.js'; +import { parseDate } from '../lib/dates.js'; import type { CommandOptions } from '../types/index.js'; export function createCategoriesCommand(): Command { @@ -39,7 +40,7 @@ export function createCategoriesCommand(): Command { .command('budget') .description('Set category budgeted amount for a month (overrides existing amount)') .argument('', 'Category ID') - .requiredOption('--month ', 'Month in YYYY-MM-DD format (e.g., 2025-07-01)') + .requiredOption('--month ', 'Budget month (e.g., 2025-07-01)') .requiredOption('--amount ', 'Total budgeted amount to set (e.g., 100.50)', parseFloat) .option('-b, --budget ', 'Budget ID') .action( @@ -58,7 +59,7 @@ export function createCategoriesCommand(): Command { const milliunits = amountToMilliunits(options.amount); const category = await client.updateMonthCategory( - options.month, + parseDate(options.month), id, { category: { budgeted: milliunits } }, options.budget @@ -73,7 +74,7 @@ export function createCategoriesCommand(): Command { .description('List transactions for category') .argument('', 'Category ID') .option('-b, --budget ', 'Budget ID') - .option('--since ', 'Filter transactions since date (YYYY-MM-DD)') + .option('--since ', 'Filter transactions since date') .option('--type ', 'Filter by transaction type') .option('--last-knowledge ', 'Last knowledge of server', parseInt) .action( @@ -89,7 +90,7 @@ export function createCategoriesCommand(): Command { ) => { const result = await client.getTransactionsByCategory(id, { budgetId: options.budget, - sinceDate: options.since, + sinceDate: options.since ? parseDate(options.since) : undefined, type: options.type, lastKnowledgeOfServer: options.lastKnowledge, }); diff --git a/src/commands/months.ts b/src/commands/months.ts index 9b8bf24..e18e7aa 100644 --- a/src/commands/months.ts +++ b/src/commands/months.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; import { withErrorHandling } from '../lib/command-utils.js'; +import { parseDate } from '../lib/dates.js'; import type { CommandOptions } from '../types/index.js'; export function createMonthsCommand(): Command { @@ -24,11 +25,11 @@ export function createMonthsCommand(): Command { cmd .command('view') .description('View specific month details') - .argument('', 'Month in YYYY-MM-DD format (e.g., 2025-07-01)') + .argument('', 'Budget month (e.g., 2025-07-01)') .option('-b, --budget ', 'Budget ID') .action( withErrorHandling(async (month: string, options: CommandOptions) => { - const monthData = await client.getBudgetMonth(month, options.budget); + const monthData = await client.getBudgetMonth(parseDate(month), options.budget); outputJson(monthData); }) ); diff --git a/src/commands/payees.ts b/src/commands/payees.ts index 69919f5..13c2e85 100644 --- a/src/commands/payees.ts +++ b/src/commands/payees.ts @@ -3,6 +3,7 @@ import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; import { YnabCliError } from '../lib/errors.js'; import { withErrorHandling } from '../lib/command-utils.js'; +import { parseDate } from '../lib/dates.js'; import type { CommandOptions } from '../types/index.js'; export function createPayeesCommand(): Command { @@ -74,7 +75,7 @@ export function createPayeesCommand(): Command { .description('List transactions for payee') .argument('', 'Payee ID') .option('-b, --budget ', 'Budget ID') - .option('--since ', 'Filter transactions since date (YYYY-MM-DD)') + .option('--since ', 'Filter transactions since date') .option('--type ', 'Filter by transaction type') .option('--last-knowledge ', 'Last knowledge of server', parseInt) .action( @@ -90,7 +91,7 @@ export function createPayeesCommand(): Command { ) => { const result = await client.getTransactionsByPayee(id, { budgetId: options.budget, - sinceDate: options.since, + sinceDate: options.since ? parseDate(options.since) : undefined, type: options.type, lastKnowledgeOfServer: options.lastKnowledge, }); diff --git a/src/commands/scheduled.ts b/src/commands/scheduled.ts index 2c33224..7e573cb 100644 --- a/src/commands/scheduled.ts +++ b/src/commands/scheduled.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; -import { withErrorHandling, confirmDelete } from '../lib/command-utils.js'; +import { withErrorHandling, requireConfirmation } from '../lib/command-utils.js'; import type { CommandOptions } from '../types/index.js'; export function createScheduledCommand(): Command { @@ -45,10 +45,7 @@ export function createScheduledCommand(): Command { .action( withErrorHandling( async (id: string, options: { budget?: string; yes?: boolean } & CommandOptions) => { - if (!(await confirmDelete('scheduled transaction', options.yes))) { - return; - } - + requireConfirmation('scheduled transaction', options.yes); const scheduledTransaction = await client.deleteScheduledTransaction(id, options.budget); outputJson({ message: 'Scheduled transaction deleted', diff --git a/src/commands/transactions.ts b/src/commands/transactions.ts index 84deacd..9232b03 100644 --- a/src/commands/transactions.ts +++ b/src/commands/transactions.ts @@ -2,16 +2,15 @@ import { Command } from 'commander'; import { client } from '../lib/api-client.js'; import { outputJson } from '../lib/output.js'; import { YnabCliError } from '../lib/errors.js'; -import { promptForTransaction } from '../lib/prompts.js'; import { - isInteractive, amountToMilliunits, applyTransactionFilters, applyFieldSelection, type TransactionLike, } from '../lib/utils.js'; -import { withErrorHandling, confirmDelete, buildUpdateObject } from '../lib/command-utils.js'; -import { validateJson, TransactionSplitSchema } from '../lib/schemas.js'; +import { withErrorHandling, requireConfirmation, buildUpdateObject } from '../lib/command-utils.js'; +import { validateTransactionSplits } from '../lib/schemas.js'; +import { parseDate, todayDate } from '../lib/dates.js'; import type { CommandOptions } from '../types/index.js'; interface TransactionOptions { @@ -36,7 +35,7 @@ function buildTransactionData(options: TransactionOptions): Record', 'Filter by account ID') .option('--category ', 'Filter by category ID') .option('--payee ', 'Filter by payee ID') - .option('--since ', 'Filter transactions since date (YYYY-MM-DD)') - .option('--until ', 'Filter transactions until date (YYYY-MM-DD)') + .option('--since ', 'Filter transactions since date') + .option('--until ', 'Filter transactions until date') .option('--type ', 'Filter by transaction type') .option('--approved ', 'Filter by approval status: true or false') .option( @@ -91,7 +90,7 @@ export function createTransactionsCommand(): Command { ) => { const params = { budgetId: options.budget, - sinceDate: options.since, + sinceDate: options.since ? parseDate(options.since) : undefined, type: options.type, }; @@ -106,7 +105,7 @@ export function createTransactionsCommand(): Command { const transactions = result?.transactions || []; const filtered = applyTransactionFilters(transactions as TransactionLike[], { - until: options.until, + until: options.until ? parseDate(options.until) : undefined, approved: options.approved, status: options.status, minAmount: options.minAmount, @@ -137,7 +136,7 @@ export function createTransactionsCommand(): Command { .description('Create transaction') .option('-b, --budget ', 'Budget ID') .option('--account ', 'Account ID') - .option('--date ', 'Date (YYYY-MM-DD)') + .option('--date ', 'Transaction date') .option('--amount ', 'Amount in currency units (e.g., 10.50)', parseFloat) .option('--payee-name ', 'Payee name') .option('--payee-id ', 'Payee ID') @@ -161,17 +160,7 @@ export function createTransactionsCommand(): Command { approved?: boolean; } & CommandOptions ) => { - const shouldPrompt = isInteractive() && !options.amount; - if (shouldPrompt && !options.account) { - throw new YnabCliError( - '--account is required. Interactive mode cannot auto-select an account.', - 400 - ); - } - const transactionData = shouldPrompt - ? { ...(await promptForTransaction()), account_id: options.account } - : buildTransactionData(options); - + const transactionData = buildTransactionData(options); const transaction = await client.createTransaction( { transaction: transactionData }, options.budget @@ -187,7 +176,7 @@ export function createTransactionsCommand(): Command { .argument('', 'Transaction ID') .option('-b, --budget ', 'Budget ID') .option('--account ', 'Account ID') - .option('--date ', 'Date (YYYY-MM-DD)') + .option('--date ', 'Transaction date') .option('--amount ', 'Amount in currency units', parseFloat) .option('--payee-name ', 'Payee name') .option('--payee-id ', 'Payee ID') @@ -232,10 +221,7 @@ export function createTransactionsCommand(): Command { .action( withErrorHandling( async (id: string, options: { budget?: string; yes?: boolean } & CommandOptions) => { - if (!(await confirmDelete('transaction', options.yes))) { - return; - } - + requireConfirmation('transaction', options.yes); const transaction = await client.deleteTransaction(id, options.budget); outputJson({ message: 'Transaction deleted', transaction }); } @@ -278,7 +264,7 @@ export function createTransactionsCommand(): Command { throw new YnabCliError('Invalid JSON in --splits parameter', 400); } - const splits = validateJson(parsedSplits, TransactionSplitSchema, 'transaction splits'); + const splits = validateTransactionSplits(parsedSplits); const splitsInMilliunits = splits.map((split) => ({ ...split, @@ -343,8 +329,8 @@ export function createTransactionsCommand(): Command { .option('--memo ', 'Search in memo field') .option('--payee-name ', 'Search in payee name') .option('--amount ', 'Search for exact amount in currency units', parseFloat) - .option('--since ', 'Search transactions since date (YYYY-MM-DD)') - .option('--until ', 'Search transactions until date (YYYY-MM-DD)') + .option('--since ', 'Search transactions since date') + .option('--until ', 'Search transactions until date') .option('--approved ', 'Filter by approval status: true or false') .option( '--status ', @@ -375,7 +361,7 @@ export function createTransactionsCommand(): Command { const params = { budgetId: options.budget, - sinceDate: options.since, + sinceDate: options.since ? parseDate(options.since) : undefined, }; const result = await client.getTransactions(params); @@ -399,7 +385,7 @@ export function createTransactionsCommand(): Command { } transactions = applyTransactionFilters(transactions, { - until: options.until, + until: options.until ? parseDate(options.until) : undefined, approved: options.approved, status: options.status, }); diff --git a/src/lib/command-utils.ts b/src/lib/command-utils.ts index 9fee1ed..d5101db 100644 --- a/src/lib/command-utils.ts +++ b/src/lib/command-utils.ts @@ -1,7 +1,4 @@ -import { handleYnabError } from './errors.js'; -import { promptForConfirmation } from './prompts.js'; -import { isInteractive } from './utils.js'; -import { outputJson } from './output.js'; +import { handleYnabError, YnabCliError } from './errors.js'; export function withErrorHandling( fn: (...args: TArgs) => Promise @@ -15,24 +12,13 @@ export function withErrorHandling( }; } -export async function confirmDelete( - itemType: string, - skipConfirmation: boolean = false -): Promise { - if (skipConfirmation || !isInteractive()) { - return true; - } - - const confirmed = await promptForConfirmation( - `Are you sure you want to delete this ${itemType}?` - ); - +export function requireConfirmation(itemType: string, confirmed: boolean = false): void { if (!confirmed) { - outputJson({ message: 'Operation cancelled' }); - return false; + throw new YnabCliError( + `Deleting ${itemType} requires --yes flag to confirm`, + 400 + ); } - - return true; } export function buildUpdateObject( diff --git a/src/lib/dates.ts b/src/lib/dates.ts new file mode 100644 index 0000000..8aad79e --- /dev/null +++ b/src/lib/dates.ts @@ -0,0 +1,14 @@ +import dayjs from 'dayjs'; +import { YnabCliError } from './errors.js'; + +export function parseDate(input: string): string { + const d = dayjs(input); + if (!d.isValid()) { + throw new YnabCliError(`Invalid date: ${input}`, 400); + } + return d.format('YYYY-MM-DD'); +} + +export function todayDate(): string { + return dayjs().format('YYYY-MM-DD'); +} diff --git a/src/lib/prompts.ts b/src/lib/prompts.ts deleted file mode 100644 index 85e2e71..0000000 --- a/src/lib/prompts.ts +++ /dev/null @@ -1,60 +0,0 @@ -import inquirer from 'inquirer'; -import { amountToMilliunits } from './utils.js'; - -export async function promptForAccessToken(): Promise { - const { token } = await inquirer.prompt([ - { - type: 'password', - name: 'token', - message: 'Enter your YNAB Personal Access Token:', - validate: (input: string) => !!input.trim() || 'Access token is required', - }, - ]); - return token.trim(); -} - -export async function promptForConfirmation(message: string): Promise { - const { confirmed } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmed', - message, - default: false, - }, - ]); - return confirmed; -} - -export async function promptForTransaction(): Promise> { - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'date', - message: 'Date (YYYY-MM-DD):', - default: new Date().toISOString().split('T')[0], - }, - { - type: 'input', - name: 'amount', - message: 'Amount (in currency units, e.g., 10.50):', - validate: (input: string) => !isNaN(parseFloat(input)) || 'Amount must be a valid number', - }, - { - type: 'input', - name: 'payee_name', - message: 'Payee name (optional):', - }, - { - type: 'input', - name: 'memo', - message: 'Memo (optional):', - }, - ]); - - return { - date: answers.date, - amount: amountToMilliunits(parseFloat(answers.amount)), - payee_name: answers.payee_name || undefined, - memo: answers.memo || undefined, - }; -} diff --git a/src/lib/schemas.test.ts b/src/lib/schemas.test.ts deleted file mode 100644 index 8825c11..0000000 --- a/src/lib/schemas.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { validateJson, TransactionSplitSchema, ApiDataSchema } from './schemas.js'; -import { YnabCliError } from './errors.js'; - -describe('TransactionSplitSchema', () => { - it('should validate valid transaction splits', () => { - const validSplits = [ - { - amount: -21400, - category_id: '123e4567-e89b-12d3-a456-426614174000', - memo: 'Split 1', - }, - { - amount: -10000, - category_id: null, - memo: 'Split 2', - }, - ]; - - const result = validateJson(validSplits, TransactionSplitSchema, 'splits'); - expect(result).toEqual(validSplits); - }); - - it('should reject splits with invalid amount type', () => { - const invalidSplits = [ - { - amount: 'not-a-number', - category_id: '123e4567-e89b-12d3-a456-426614174000', - }, - ]; - - expect(() => { - validateJson(invalidSplits, TransactionSplitSchema, 'splits'); - }).toThrow(YnabCliError); - }); - - it('should allow optional fields to be missing', () => { - const minimalSplits = [ - { - amount: -5000, - }, - ]; - - const result = validateJson(minimalSplits, TransactionSplitSchema, 'splits'); - expect(result).toEqual(minimalSplits); - }); - - it('should reject non-array input', () => { - const notAnArray = { - amount: -5000, - category_id: '123e4567-e89b-12d3-a456-426614174000', - }; - - expect(() => { - validateJson(notAnArray, TransactionSplitSchema, 'splits'); - }).toThrow(YnabCliError); - }); - - it('should handle empty arrays', () => { - const emptySplits: unknown[] = []; - const result = validateJson(emptySplits, TransactionSplitSchema, 'splits'); - expect(result).toEqual([]); - }); -}); - -describe('ApiDataSchema', () => { - it('should validate valid objects', () => { - const validData = { - transaction: { - date: '2024-01-01', - amount: -10000, - }, - }; - - const result = validateJson(validData, ApiDataSchema, 'API data'); - expect(result).toEqual(validData); - }); - - it('should accept empty objects', () => { - const emptyData = {}; - const result = validateJson(emptyData, ApiDataSchema, 'API data'); - expect(result).toEqual({}); - }); - - it('should accept nested objects', () => { - const nestedData = { - level1: { - level2: { - level3: 'value', - }, - }, - }; - - const result = validateJson(nestedData, ApiDataSchema, 'API data'); - expect(result).toEqual(nestedData); - }); - - it('should reject non-object types', () => { - const notAnObject = 'just a string'; - - expect(() => { - validateJson(notAnObject, ApiDataSchema, 'API data'); - }).toThrow(YnabCliError); - }); - - it('should reject arrays', () => { - const anArray = [1, 2, 3]; - - expect(() => { - validateJson(anArray, ApiDataSchema, 'API data'); - }).toThrow(YnabCliError); - }); -}); - -describe('validateJson', () => { - it('should include field name in error message', () => { - const invalidData = 'not valid'; - - try { - validateJson(invalidData, ApiDataSchema, 'test field'); - expect.fail('Should have thrown an error'); - } catch (error) { - expect(error).toBeInstanceOf(YnabCliError); - expect((error as YnabCliError).message).toContain('test field'); - } - }); - - it('should include validation details in error message', () => { - const invalidSplits = [{ amount: 'invalid' }]; - - try { - validateJson(invalidSplits, TransactionSplitSchema, 'splits'); - expect.fail('Should have thrown an error'); - } catch (error) { - expect(error).toBeInstanceOf(YnabCliError); - expect((error as YnabCliError).message).toContain('amount'); - } - }); -}); diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 2c67079..fe90ef0 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,25 +1,40 @@ -import { z } from 'zod'; import { YnabCliError } from './errors.js'; -export const TransactionSplitSchema = z.array( - z.object({ - amount: z.number(), - category_id: z.string().nullable().optional(), - memo: z.string().optional(), - payee_id: z.string().optional(), - }) -); +export interface TransactionSplit { + amount: number; + category_id?: string | null; + memo?: string; + payee_id?: string; +} + +export function validateTransactionSplits(data: unknown): TransactionSplit[] { + if (!Array.isArray(data)) { + throw new YnabCliError('Transaction splits must be an array', 400); + } + + return data.map((item, index) => { + if (typeof item !== 'object' || item === null) { + throw new YnabCliError(`Split at index ${index} must be an object`, 400); + } -export const ApiDataSchema = z.record(z.any()); + const split = item as Record; -export function validateJson(data: unknown, schema: z.ZodSchema, fieldName: string): T { - try { - return schema.parse(data); - } catch (error) { - if (error instanceof z.ZodError) { - const issues = error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', '); - throw new YnabCliError(`Invalid ${fieldName}: ${issues}`, 400); + if (typeof split.amount !== 'number') { + throw new YnabCliError(`Split at index ${index} must have a numeric amount`, 400); } - throw error; + + return { + amount: split.amount, + category_id: split.category_id as string | null | undefined, + memo: split.memo as string | undefined, + payee_id: split.payee_id as string | undefined, + }; + }); +} + +export function validateApiData(data: unknown): Record { + if (typeof data !== 'object' || data === null || Array.isArray(data)) { + throw new YnabCliError('API data must be an object', 400); } + return data as Record; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f3462dc..9cf1a0d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -8,10 +8,6 @@ export function amountToMilliunits(amount: number): number { return Math.round(amount * 1000); } -export function isInteractive(): boolean { - return process.stdin.isTTY === true; -} - export function convertMilliunitsToAmounts(data: unknown): unknown { if (data === null || data === undefined) return data; if (Array.isArray(data)) return data.map(convertMilliunitsToAmounts);