From cb3375b8c3e4305a6947e028e36d7e555382d101 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 5 Jan 2026 17:34:47 +0200 Subject: [PATCH 1/4] add naive implementation of login in cli - no http calls yet --- package-lock.json | 458 ++++++++++++++++++++++++++++++++ package.json | 4 +- src/cli/commands/auth/login.ts | 140 ++++++++-- src/cli/commands/auth/logout.ts | 2 +- src/cli/commands/auth/whoami.ts | 3 +- src/cli/utils/runCommand.ts | 13 +- src/core/api/auth/client.ts | 108 ++++++++ src/core/api/auth/index.ts | 4 + src/core/api/auth/schema.ts | 19 ++ src/core/api/index.ts | 2 + src/core/errors/errors.ts | 20 ++ src/core/errors/index.ts | 2 + src/core/index.ts | 2 + tsconfig.json | 9 +- 14 files changed, 761 insertions(+), 25 deletions(-) create mode 100644 src/core/api/auth/client.ts create mode 100644 src/core/api/auth/index.ts create mode 100644 src/core/api/auth/schema.ts create mode 100644 src/core/api/index.ts create mode 100644 src/core/errors/errors.ts create mode 100644 src/core/errors/index.ts diff --git a/package-lock.json b/package-lock.json index 077cb788..8c19b398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@clack/prompts": "^0.11.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "p-wait-for": "^6.0.0", "zod": "^4.3.5" }, "bin": { @@ -23,6 +24,7 @@ "@typescript-eslint/parser": "^8.51.0", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", + "tsc-alias": "^1.8.16", "tsx": "^4.19.2", "typescript": "^5.7.2" }, @@ -262,6 +264,41 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, @@ -571,6 +608,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -612,6 +674,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", "dev": true, @@ -713,6 +784,18 @@ "dev": true, "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "dev": true, @@ -721,6 +804,18 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "dev": true, @@ -783,6 +878,42 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -925,6 +1056,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -1391,6 +1534,34 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, @@ -1401,6 +1572,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "dev": true, @@ -1428,6 +1608,18 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "dev": true, @@ -1628,6 +1820,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "dev": true, @@ -1811,6 +2023,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "dev": true, @@ -1955,6 +2179,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "dev": true, @@ -2191,6 +2424,40 @@ "node": ">= 0.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "dev": true, @@ -2224,11 +2491,33 @@ "dev": true, "license": "MIT" }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "dev": true, @@ -2374,6 +2663,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-wait-for": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-6.0.0.tgz", + "integrity": "sha512-2kKzMtjS8TVcpCOU/gr3vZ4K/WIyS1AsEFXFWapM/0lERCdyTbB6ZeuCIp+cL1aeLZfQoMdZFCBTHiK4I9UtOw==", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -2406,6 +2706,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -2421,6 +2730,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "dev": true, @@ -2445,6 +2766,59 @@ "node": ">=6" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "dev": true, @@ -2520,6 +2894,39 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "dev": true, @@ -2711,6 +3118,15 @@ "version": "1.0.5", "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "dev": true, @@ -2832,6 +3248,18 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "dev": true, @@ -2843,6 +3271,36 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "dev": true, diff --git a/package.json b/package.json index 631a1491..5473f148 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "tsc", + "build": "tsc && tsc-alias", "dev": "tsx src/cli/index.ts", "start": "node dist/cli/index.js", "clean": "rm -rf dist", @@ -37,6 +37,7 @@ "@clack/prompts": "^0.11.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "p-wait-for": "^6.0.0", "zod": "^4.3.5" }, "devDependencies": { @@ -45,6 +46,7 @@ "@typescript-eslint/parser": "^8.51.0", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", + "tsc-alias": "^1.8.16", "tsx": "^4.19.2", "typescript": "^5.7.2" }, diff --git a/src/cli/commands/auth/login.ts b/src/cli/commands/auth/login.ts index a3a4c2cd..f691cf61 100644 --- a/src/cli/commands/auth/login.ts +++ b/src/cli/commands/auth/login.ts @@ -1,23 +1,130 @@ import { Command } from "commander"; -import { tasks } from "@clack/prompts"; -import { writeAuth } from "../../../core/config/auth.js"; +import { log, spinner } from "@clack/prompts"; +import pWaitFor from "p-wait-for"; +import { writeAuth } from "@config/auth.js"; +import { + generateDeviceCode, + getTokenFromDeviceCode, + AuthApiError, + AuthValidationError, + type DeviceCodeResponse, + type TokenResponse, +} from "@api/auth"; import { runCommand } from "../../utils/index.js"; -async function login(): Promise { - await tasks([ - { - title: "Logging you in", - task: async () => { - await writeAuth({ - token: "stub-token-12345", - email: "valid@email.com", - name: "KfirStri", - }); - - return "Logged in as KfirStri"; +async function generateAndDisplayDeviceCode(): Promise { + const s = spinner(); + s.start("Generating device code..."); + + try { + const deviceCodeResponse = await generateDeviceCode(); + s.stop("Device code generated"); + + log.info( + `Please visit: ${deviceCodeResponse.verificationUrl}\n` + + `Enter your device code: ${deviceCodeResponse.userCode}` + ); + + return deviceCodeResponse; + } catch (error) { + s.stop("Failed to generate device code"); + if (error instanceof AuthValidationError) { + const issues = error.issues.map((i) => i.message).join(", "); + throw new Error(`Invalid response from server: ${issues}`); + } + if (error instanceof AuthApiError) { + throw new Error(`Failed to generate device code: ${error.message}`); + } + throw new Error( + `Unexpected error: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} + +async function waitForAuthentication( + deviceCode: string, + expiresIn: number +): Promise { + const s = spinner(); + s.start("Waiting for you to complete authentication..."); + + let tokenResponse: TokenResponse | null = null; + + try { + await pWaitFor( + async () => { + try { + const result = await getTokenFromDeviceCode(deviceCode); + if (result !== null) { + tokenResponse = result; + return true; + } + return false; + } catch (error) { + if (error instanceof AuthValidationError) { + const issues = error.issues.map((i) => i.message).join(", "); + throw new Error(`Invalid response from server: ${issues}`); + } + if (error instanceof AuthApiError) { + throw new Error(`API error: ${error.message}`); + } + throw error; + } }, - }, - ]); + { + interval: 2000, + timeout: expiresIn * 1000, + } + ); + } catch (error) { + s.stop("Authentication failed"); + if (error instanceof Error && error.message.includes("timed out")) { + throw new Error("Authentication timed out. Please try again."); + } + if (error instanceof Error) { + throw error; + } + throw new Error("Unexpected error during authentication"); + } + + s.stop("Authentication completed!"); + + if (!tokenResponse) { + throw new Error("Failed to retrieve authentication token."); + } + + return tokenResponse; +} + +async function saveAuthData(token: TokenResponse): Promise { + try { + await writeAuth({ + token: token.token, + email: token.email, + name: token.name, + }); + } catch (error) { + throw new Error( + `Failed to save authentication data: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } +} + +async function login(): Promise { + const deviceCodeResponse = await generateAndDisplayDeviceCode(); + + const token = await waitForAuthentication( + deviceCodeResponse.deviceCode, + deviceCodeResponse.expiresIn + ); + + await saveAuthData(token); + + log.success(`Logged in as ${token.name}`); } export const loginCommand = new Command("login") @@ -25,4 +132,3 @@ export const loginCommand = new Command("login") .action(async () => { await runCommand(login); }); - diff --git a/src/cli/commands/auth/logout.ts b/src/cli/commands/auth/logout.ts index 26098c88..1b0f3bd0 100644 --- a/src/cli/commands/auth/logout.ts +++ b/src/cli/commands/auth/logout.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { log } from "@clack/prompts"; -import { deleteAuth } from "../../../core/config/auth.js"; +import { deleteAuth } from "@config/auth.js"; import { runCommand } from "../../utils/index.js"; async function logout(): Promise { diff --git a/src/cli/commands/auth/whoami.ts b/src/cli/commands/auth/whoami.ts index fdeb8db8..1382f948 100644 --- a/src/cli/commands/auth/whoami.ts +++ b/src/cli/commands/auth/whoami.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { log } from "@clack/prompts"; -import { readAuth } from "../../../core/config/auth.js"; +import { readAuth } from "@config/auth.js"; import { runCommand } from "../../utils/index.js"; async function whoami(): Promise { @@ -21,4 +21,3 @@ export const whoamiCommand = new Command("whoami") .action(async () => { await runCommand(whoami); }); - diff --git a/src/cli/utils/runCommand.ts b/src/cli/utils/runCommand.ts index 83e7ef93..91c72cae 100644 --- a/src/cli/utils/runCommand.ts +++ b/src/cli/utils/runCommand.ts @@ -9,8 +9,15 @@ const base44Color = chalk.bgHex("#E86B3C"); * * @param commandFn - The async function to execute as the command */ -export async function runCommand(commandFn: () => Promise): Promise { +export async function runCommand( + commandFn: () => Promise +): Promise { intro(base44Color(" Base 44 ")); - await commandFn(); -} + try { + await commandFn(); + } catch (e) { + console.log("reporting error..."); + console.error(e); + } +} diff --git a/src/core/api/auth/client.ts b/src/core/api/auth/client.ts new file mode 100644 index 00000000..938f768b --- /dev/null +++ b/src/core/api/auth/client.ts @@ -0,0 +1,108 @@ +import { + DeviceCodeResponseSchema, + type DeviceCodeResponse, + TokenResponseSchema, + type TokenResponse, +} from './schema.js'; +import { AuthApiError, AuthValidationError } from '@core/errors/index.js'; + +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const deviceCodeToTokenMap = new Map< + string, + { startTime: number; readyAfter: number } +>(); + +export async function generateDeviceCode(): Promise { + try { + await delay(1000); + + const deviceCode = `device-code-${Date.now()}`; + + deviceCodeToTokenMap.set(deviceCode, { + startTime: Date.now(), + readyAfter: 5000, + }); + + const mockResponse: DeviceCodeResponse = { + deviceCode, + userCode: "ABCD-1234", + verificationUrl: "https://app.base44.com/verify", + expiresIn: 600, + }; + + const result = DeviceCodeResponseSchema.safeParse(mockResponse); + if (!result.success) { + throw new AuthValidationError( + "Invalid device code response from server", + result.error.issues.map((issue) => ({ + message: issue.message, + path: issue.path.map(String), + })) + ); + } + + return result.data; + } catch (error) { + if (error instanceof AuthValidationError) { + throw error; + } + throw new AuthApiError( + "Failed to generate device code", + error instanceof Error ? error : new Error(String(error)) + ); + } +} + +export async function getTokenFromDeviceCode( + deviceCode: string +): Promise { + try { + await delay(1000); + + const deviceInfo = deviceCodeToTokenMap.get(deviceCode); + + if (!deviceInfo) { + return null; + } + + const elapsed = Date.now() - deviceInfo.startTime; + + if (elapsed < deviceInfo.readyAfter) { + return null; + } + + const mockResponse: TokenResponse = { + token: "mock-token-" + Date.now(), + email: "user@example.com", + name: "Test User", + }; + + const result = TokenResponseSchema.safeParse(mockResponse); + if (!result.success) { + throw new AuthValidationError( + "Invalid token response from server", + result.error.issues.map((issue) => ({ + message: issue.message, + path: issue.path.map(String), + })) + ); + } + + deviceCodeToTokenMap.delete(deviceCode); + return result.data; + } catch (error) { + if (error instanceof AuthValidationError) { + throw error; + } + if (error instanceof AuthApiError) { + throw error; + } + throw new AuthApiError( + "Failed to retrieve token from device code", + error instanceof Error ? error : new Error(String(error)) + ); + } +} diff --git a/src/core/api/auth/index.ts b/src/core/api/auth/index.ts new file mode 100644 index 00000000..dc3d07c2 --- /dev/null +++ b/src/core/api/auth/index.ts @@ -0,0 +1,4 @@ +export * from './client.js'; +export * from './schema.js'; +export { AuthApiError, AuthValidationError } from '@core/errors/index.js'; + diff --git a/src/core/api/auth/schema.ts b/src/core/api/auth/schema.ts new file mode 100644 index 00000000..2dec8dec --- /dev/null +++ b/src/core/api/auth/schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +export const DeviceCodeResponseSchema = z.object({ + deviceCode: z.string().min(1, 'Device code cannot be empty'), + userCode: z.string().min(1, 'User code cannot be empty'), + verificationUrl: z.url('Invalid verification URL'), + expiresIn: z.number().int().positive('Expires in must be a positive integer'), +}); + +export type DeviceCodeResponse = z.infer; + +export const TokenResponseSchema = z.object({ + token: z.string().min(1, 'Token cannot be empty'), + email: z.email('Invalid email address'), + name: z.string().min(1, 'Name cannot be empty'), +}); + +export type TokenResponse = z.infer; + diff --git a/src/core/api/index.ts b/src/core/api/index.ts new file mode 100644 index 00000000..66326866 --- /dev/null +++ b/src/core/api/index.ts @@ -0,0 +1,2 @@ +export * from './auth/index.js'; + diff --git a/src/core/errors/errors.ts b/src/core/errors/errors.ts new file mode 100644 index 00000000..20d05eda --- /dev/null +++ b/src/core/errors/errors.ts @@ -0,0 +1,20 @@ +export class AuthApiError extends Error { + constructor( + message: string, + public readonly cause?: unknown + ) { + super(message); + this.name = 'AuthApiError'; + } +} + +export class AuthValidationError extends Error { + constructor( + message: string, + public readonly issues: Array<{ message: string; path: string[] }> + ) { + super(message); + this.name = 'AuthValidationError'; + } +} + diff --git a/src/core/errors/index.ts b/src/core/errors/index.ts new file mode 100644 index 00000000..441415ad --- /dev/null +++ b/src/core/errors/index.ts @@ -0,0 +1,2 @@ +export * from './errors.js'; + diff --git a/src/core/index.ts b/src/core/index.ts index 7d3f41c1..c1dc5d77 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,6 @@ export * from './schemas/index.js'; export * from './config/index.js'; export * from './utils/index.js'; +export * from './api/index.js'; +export * from './errors/index.js'; diff --git a/tsconfig.json b/tsconfig.json index 09c771e1..ded1bb3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,14 @@ "sourceMap": true, "outDir": "./dist", "rootDir": "./src", - "types": ["node"] + "types": ["node"], + "baseUrl": ".", + "paths": { + "@api/*": ["./src/core/api/*"], + "@schemas/*": ["./src/core/schemas/*"], + "@config/*": ["./src/core/config/*"], + "@core/*": ["./src/core/*"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] From ed4e59c0bb77c3b63a99db61bf8f5071e3374f34 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 5 Jan 2026 17:46:51 +0200 Subject: [PATCH 2/4] change the general command to exit with an error --- src/cli/utils/runCommand.ts | 6 +++--- src/core/api/auth/client.ts | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/cli/utils/runCommand.ts b/src/cli/utils/runCommand.ts index 91c72cae..6036f1d8 100644 --- a/src/cli/utils/runCommand.ts +++ b/src/cli/utils/runCommand.ts @@ -1,4 +1,4 @@ -import { intro } from "@clack/prompts"; +import { intro, log } from "@clack/prompts"; import chalk from "chalk"; const base44Color = chalk.bgHex("#E86B3C"); @@ -17,7 +17,7 @@ export async function runCommand( try { await commandFn(); } catch (e) { - console.log("reporting error..."); - console.error(e); + log.error(e instanceof Error ? e.message : String(e)); + process.exit(1); } } diff --git a/src/core/api/auth/client.ts b/src/core/api/auth/client.ts index 938f768b..1f80c91c 100644 --- a/src/core/api/auth/client.ts +++ b/src/core/api/auth/client.ts @@ -3,8 +3,8 @@ import { type DeviceCodeResponse, TokenResponseSchema, type TokenResponse, -} from './schema.js'; -import { AuthApiError, AuthValidationError } from '@core/errors/index.js'; +} from "./schema.js"; +import { AuthApiError, AuthValidationError } from "@core/errors/index.js"; async function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -76,7 +76,7 @@ export async function getTokenFromDeviceCode( const mockResponse: TokenResponse = { token: "mock-token-" + Date.now(), - email: "user@example.com", + email: "sd.com", name: "Test User", }; @@ -94,12 +94,10 @@ export async function getTokenFromDeviceCode( deviceCodeToTokenMap.delete(deviceCode); return result.data; } catch (error) { - if (error instanceof AuthValidationError) { - throw error; - } - if (error instanceof AuthApiError) { + if (error instanceof AuthValidationError || error instanceof AuthApiError) { throw error; } + throw new AuthApiError( "Failed to retrieve token from device code", error instanceof Error ? error : new Error(String(error)) From 73d98cd2912ef83e19842a70e49fac63976227f1 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 5 Jan 2026 17:50:35 +0200 Subject: [PATCH 3/4] bump package lock --- package-lock.json | 2 +- package.json | 2 +- src/core/api/auth/client.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c19b398..16050c7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "typescript": "^5.7.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@clack/core": { diff --git a/package.json b/package.json index 5473f148..be8c97f9 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,6 @@ "typescript": "^5.7.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/src/core/api/auth/client.ts b/src/core/api/auth/client.ts index 1f80c91c..cb93dfba 100644 --- a/src/core/api/auth/client.ts +++ b/src/core/api/auth/client.ts @@ -76,7 +76,7 @@ export async function getTokenFromDeviceCode( const mockResponse: TokenResponse = { token: "mock-token-" + Date.now(), - email: "sd.com", + email: "stam@lala.com", name: "Test User", }; @@ -97,7 +97,6 @@ export async function getTokenFromDeviceCode( if (error instanceof AuthValidationError || error instanceof AuthApiError) { throw error; } - throw new AuthApiError( "Failed to retrieve token from device code", error instanceof Error ? error : new Error(String(error)) From 9824e2f94c9418b64296d88be672e1f1721067fb Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Tue, 6 Jan 2026 12:54:29 +0200 Subject: [PATCH 4/4] refactor spinner management and errors --- src/cli/commands/auth/login.ts | 112 ++++++++++++-------------------- src/cli/commands/auth/logout.ts | 12 +--- src/cli/commands/auth/whoami.ts | 12 +--- src/cli/utils/index.ts | 1 + src/cli/utils/runCommand.ts | 12 +++- src/cli/utils/runTask.ts | 32 +++++++++ 6 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 src/cli/utils/runTask.ts diff --git a/src/cli/commands/auth/login.ts b/src/cli/commands/auth/login.ts index f691cf61..980efb17 100644 --- a/src/cli/commands/auth/login.ts +++ b/src/cli/commands/auth/login.ts @@ -1,96 +1,72 @@ import { Command } from "commander"; -import { log, spinner } from "@clack/prompts"; +import { log } from "@clack/prompts"; import pWaitFor from "p-wait-for"; import { writeAuth } from "@config/auth.js"; import { generateDeviceCode, getTokenFromDeviceCode, - AuthApiError, - AuthValidationError, type DeviceCodeResponse, type TokenResponse, } from "@api/auth"; -import { runCommand } from "../../utils/index.js"; +import { runCommand, runTask } from "../../utils/index.js"; async function generateAndDisplayDeviceCode(): Promise { - const s = spinner(); - s.start("Generating device code..."); - - try { - const deviceCodeResponse = await generateDeviceCode(); - s.stop("Device code generated"); + const deviceCodeResponse = await runTask( + "Generating device code...", + async () => { + return await generateDeviceCode(); + }, + { + successMessage: "Device code generated", + errorMessage: "Failed to generate device code", + } + ); - log.info( - `Please visit: ${deviceCodeResponse.verificationUrl}\n` + - `Enter your device code: ${deviceCodeResponse.userCode}` - ); + log.info( + `Please visit: ${deviceCodeResponse.verificationUrl}\n` + + `Enter your device code: ${deviceCodeResponse.userCode}` + ); - return deviceCodeResponse; - } catch (error) { - s.stop("Failed to generate device code"); - if (error instanceof AuthValidationError) { - const issues = error.issues.map((i) => i.message).join(", "); - throw new Error(`Invalid response from server: ${issues}`); - } - if (error instanceof AuthApiError) { - throw new Error(`Failed to generate device code: ${error.message}`); - } - throw new Error( - `Unexpected error: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } + return deviceCodeResponse; } async function waitForAuthentication( deviceCode: string, expiresIn: number ): Promise { - const s = spinner(); - s.start("Waiting for you to complete authentication..."); - let tokenResponse: TokenResponse | null = null; try { - await pWaitFor( + await runTask( + "Waiting for you to complete authentication...", async () => { - try { - const result = await getTokenFromDeviceCode(deviceCode); - if (result !== null) { - tokenResponse = result; - return true; - } - return false; - } catch (error) { - if (error instanceof AuthValidationError) { - const issues = error.issues.map((i) => i.message).join(", "); - throw new Error(`Invalid response from server: ${issues}`); - } - if (error instanceof AuthApiError) { - throw new Error(`API error: ${error.message}`); + await pWaitFor( + async () => { + const result = await getTokenFromDeviceCode(deviceCode); + if (result !== null) { + tokenResponse = result; + return true; + } + return false; + }, + { + interval: 2000, + timeout: expiresIn * 1000, } - throw error; - } + ); }, { - interval: 2000, - timeout: expiresIn * 1000, + successMessage: "Authentication completed!", + errorMessage: "Authentication failed", } ); } catch (error) { - s.stop("Authentication failed"); if (error instanceof Error && error.message.includes("timed out")) { throw new Error("Authentication timed out. Please try again."); } - if (error instanceof Error) { - throw error; - } - throw new Error("Unexpected error during authentication"); + throw error; } - s.stop("Authentication completed!"); - if (!tokenResponse) { throw new Error("Failed to retrieve authentication token."); } @@ -99,19 +75,11 @@ async function waitForAuthentication( } async function saveAuthData(token: TokenResponse): Promise { - try { - await writeAuth({ - token: token.token, - email: token.email, - name: token.name, - }); - } catch (error) { - throw new Error( - `Failed to save authentication data: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } + await writeAuth({ + token: token.token, + email: token.email, + name: token.name, + }); } async function login(): Promise { diff --git a/src/cli/commands/auth/logout.ts b/src/cli/commands/auth/logout.ts index 1b0f3bd0..d6252ea2 100644 --- a/src/cli/commands/auth/logout.ts +++ b/src/cli/commands/auth/logout.ts @@ -4,16 +4,8 @@ import { deleteAuth } from "@config/auth.js"; import { runCommand } from "../../utils/index.js"; async function logout(): Promise { - try { - await deleteAuth(); - log.info("Logged out successfully"); - } catch (error) { - if (error instanceof Error) { - log.error(error.message); - } else { - log.error("Failed to logout"); - } - } + await deleteAuth(); + log.info("Logged out successfully"); } export const logoutCommand = new Command("logout") diff --git a/src/cli/commands/auth/whoami.ts b/src/cli/commands/auth/whoami.ts index 1382f948..69e148d2 100644 --- a/src/cli/commands/auth/whoami.ts +++ b/src/cli/commands/auth/whoami.ts @@ -4,16 +4,8 @@ import { readAuth } from "@config/auth.js"; import { runCommand } from "../../utils/index.js"; async function whoami(): Promise { - try { - const auth = await readAuth(); - log.info(`Logged in as: ${auth.name} (${auth.email})`); - } catch (error) { - if (error instanceof Error) { - log.error(error.message); - } else { - log.error("Failed to read authentication data"); - } - } + const auth = await readAuth(); + log.info(`Logged in as: ${auth.name} (${auth.email})`); } export const whoamiCommand = new Command("whoami") diff --git a/src/cli/utils/index.ts b/src/cli/utils/index.ts index 545a13a3..8e7b5ed6 100644 --- a/src/cli/utils/index.ts +++ b/src/cli/utils/index.ts @@ -1,3 +1,4 @@ export * from "./packageVersion.js"; export * from "./runCommand.js"; +export * from "./runTask.js"; diff --git a/src/cli/utils/runCommand.ts b/src/cli/utils/runCommand.ts index 6036f1d8..8dcd13d4 100644 --- a/src/cli/utils/runCommand.ts +++ b/src/cli/utils/runCommand.ts @@ -1,5 +1,6 @@ import { intro, log } from "@clack/prompts"; import chalk from "chalk"; +import { AuthApiError, AuthValidationError } from "@core/errors/index.js"; const base44Color = chalk.bgHex("#E86B3C"); @@ -17,7 +18,16 @@ export async function runCommand( try { await commandFn(); } catch (e) { - log.error(e instanceof Error ? e.message : String(e)); + if (e instanceof AuthValidationError) { + const issues = e.issues.map((i) => i.message).join(", "); + log.error(`Invalid response from server: ${issues}`); + } else if (e instanceof AuthApiError) { + log.error(e.message); + } else if (e instanceof Error) { + log.error(e.message); + } else { + log.error(String(e)); + } process.exit(1); } } diff --git a/src/cli/utils/runTask.ts b/src/cli/utils/runTask.ts new file mode 100644 index 00000000..1aa4c9ef --- /dev/null +++ b/src/cli/utils/runTask.ts @@ -0,0 +1,32 @@ +import { spinner } from "@clack/prompts"; + +/** + * Wraps an async operation with automatic spinner management. + * The spinner is automatically started, and stopped on both success and error. + * + * @param startMessage - Message to show when spinner starts + * @param operation - The async operation to execute + * @param options - Optional configuration + * @returns The result of the operation + */ +export async function runTask( + startMessage: string, + operation: () => Promise, + options?: { + successMessage?: string; + errorMessage?: string; + } +): Promise { + const s = spinner(); + s.start(startMessage); + + try { + const result = await operation(); + s.stop(options?.successMessage || startMessage); + return result; + } catch (error) { + s.stop(options?.errorMessage || "Failed"); + throw error; + } +} +