diff --git a/.github/workflows/cli-test.yml b/.github/workflows/cli-test.yml new file mode 100644 index 000000000..024c854ca --- /dev/null +++ b/.github/workflows/cli-test.yml @@ -0,0 +1,66 @@ +name: CLI Unit Test + +on: + pull_request: + paths: + - 'apps/cli/**' + - '.github/workflows/cli-test.yml' + push: + branches: [main] + paths: + - 'apps/cli/**' + - '.github/workflows/cli-test.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.13.0 + package_json_file: apps/cli/package.json + + - name: Get pnpm store path + id: pnpm-cache + run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT + working-directory: apps/cli + + - name: Cache pnpm store + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.pnpm-cache.outputs.store }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('apps/cli/pnpm-lock.yaml') }} + restore-keys: | + pnpm-store-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: apps/cli + + - name: Type-check contract tests + run: pnpm typecheck:test + working-directory: apps/cli + + - name: Run tests + run: pnpm test + working-directory: apps/cli diff --git a/apps/cli/package.json b/apps/cli/package.json index 16f13a07b..436174e2d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,26 +1,29 @@ -{ - "name": "@koyori/task-cli", - "version": "0.1.0", - "private": true, - "engines": { - "node": ">=20" - }, - "bin": { - "task": "./bin/task.js" - }, - "scripts": { - "build": "tsc", - "dev": "ts-node src/index.ts" - }, - "dependencies": { +{ + "name": "@koyori/task-cli", + "version": "0.1.0", + "private": true, + "engines": { + "node": ">=20" + }, + "bin": { + "task": "./bin/task.js" + }, + "scripts": { + "build": "tsc", + "dev": "ts-node src/index.ts", + "test": "pnpm run build && vitest run", + "typecheck:test": "tsc -p tsconfig.test.json" + }, + "dependencies": { "commander": "14.0.3", "js-yaml": "4.3.0", "openapi-fetch": "0.17.0" - }, - "devDependencies": { - "@types/js-yaml": "4.0.9", - "@types/node": "24.13.3", - "ts-node": "10.9.2", - "typescript": "5.9.3" - } -} + }, + "devDependencies": { + "@types/js-yaml": "4.0.9", + "@types/node": "24.13.3", + "ts-node": "10.9.2", + "typescript": "5.9.3", + "vitest": "4.1.10" + } +} diff --git a/apps/cli/pnpm-lock.yaml b/apps/cli/pnpm-lock.yaml index b8ba2a18e..799d19f23 100644 --- a/apps/cli/pnpm-lock.yaml +++ b/apps/cli/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: typescript: specifier: 5.9.3 version: 5.9.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@types/node@24.13.3)(vite@8.1.4(@types/node@24.13.3)) packages: @@ -40,6 +43,15 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -50,6 +62,116 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -62,12 +184,53 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -83,30 +246,202 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + diff@4.0.4: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + openapi-fetch@0.17.0: resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} openapi-typescript-helpers@0.1.0: resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -121,6 +456,9 @@ packages: '@swc/wasm': optional: true + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -132,6 +470,95 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -142,6 +569,22 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -151,6 +594,68 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -159,12 +664,67 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + '@types/js-yaml@4.0.9': {} '@types/node@24.13.3': dependencies: undici-types: 7.18.2 + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@24.13.3))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.4(@types/node@24.13.3) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn-walk@8.3.5: dependencies: acorn: 8.17.0 @@ -175,24 +735,156 @@ snapshots: argparse@2.0.1: {} + assertion-error@2.0.1: {} + + chai@6.2.2: {} + commander@14.0.3: {} + convert-source-map@2.0.0: {} + create-require@1.1.1: {} + detect-libc@2.1.2: {} + diff@4.0.4: {} + es-module-lexer@2.3.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fsevents@2.3.3: + optional: true + js-yaml@4.3.0: dependencies: argparse: 2.0.1 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-error@1.3.6: {} + nanoid@3.3.16: {} + + obug@2.1.3: {} + openapi-fetch@0.17.0: dependencies: openapi-typescript-helpers: 0.1.0 openapi-typescript-helpers@0.1.0: {} + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -211,10 +903,56 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tslib@2.8.1: + optional: true + typescript@5.9.3: {} undici-types@7.18.2: {} v8-compile-cache-lib@3.0.1: {} + vite@8.1.4(@types/node@24.13.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + + vitest@4.1.10(@types/node@24.13.3)(vite@8.1.4(@types/node@24.13.3)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@24.13.3)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@24.13.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + yn@3.1.1: {} diff --git a/apps/cli/src/__tests__/entry.test.ts b/apps/cli/src/__tests__/entry.test.ts new file mode 100644 index 000000000..7f2c450de --- /dev/null +++ b/apps/cli/src/__tests__/entry.test.ts @@ -0,0 +1,38 @@ +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +const packageRoot = path.resolve(__dirname, "../.."); +const entry = path.join(packageRoot, "bin", "task.js"); + +function run(args: string[]) { + return spawnSync(process.execPath, [entry, ...args], { + cwd: packageRoot, + encoding: "utf8", + env: { ...process.env, NO_COLOR: "1" }, + }); +} + +describe("bin/task.js entrypoint", () => { + it("prints help and package version through the compiled index", () => { + const help = run(["--help"]); + expect(help.status).toBe(0); + expect(help.stdout).toContain("Task management CLI"); + expect(help.stdout).toContain("auth"); + expect(help.stdout).toContain("tasks"); + + const version = run(["--version"]); + expect(version.status).toBe(0); + expect(version.stdout.trim()).toBe("0.1.0"); + }); + + it.each(["auth", "config", "my", "projects", "sprints", "tasks"])( + "renders help for %s", + (command) => { + const result = run([command, "--help"]); + expect(result.status).toBe(0); + expect(result.stdout).toContain(`Usage: task ${command}`); + expect(result.stderr.trim()).toBe(""); + }, + ); +}); diff --git a/apps/cli/src/__tests__/globalSetup.ts b/apps/cli/src/__tests__/globalSetup.ts new file mode 100644 index 000000000..a9d32c313 --- /dev/null +++ b/apps/cli/src/__tests__/globalSetup.ts @@ -0,0 +1,42 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const packageRoot = path.resolve(__dirname, "../.."); +const compiledEntry = path.join(packageRoot, "dist", "index.js"); +const srcRoot = path.join(packageRoot, "src"); +const tsconfigPath = path.join(packageRoot, "tsconfig.json"); + +function newestMtimeMs(targetPath: string): number { + const stat = fs.statSync(targetPath); + if (!stat.isDirectory()) { + return stat.mtimeMs; + } + + let newest = 0; + for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) { + newest = Math.max(newest, newestMtimeMs(path.join(targetPath, entry.name))); + } + return newest; +} + +function needsRebuild(): boolean { + if (!fs.existsSync(compiledEntry)) { + return true; + } + + const distMtime = fs.statSync(compiledEntry).mtimeMs; + const srcMtime = newestMtimeMs(srcRoot); + const tsconfigMtime = fs.existsSync(tsconfigPath) + ? fs.statSync(tsconfigPath).mtimeMs + : 0; + + return srcMtime > distMtime || tsconfigMtime > distMtime; +} + +export default function globalSetup() { + if (!needsRebuild()) { + return; + } + execSync("pnpm run build", { cwd: packageRoot, stdio: "inherit" }); +} diff --git a/apps/cli/src/api/__tests__/client.test.ts b/apps/cli/src/api/__tests__/client.test.ts new file mode 100644 index 000000000..adb1772de --- /dev/null +++ b/apps/cli/src/api/__tests__/client.test.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + createClient: vi.fn(() => ({ GET: vi.fn() })), + resolveRuntimeConfig: vi.fn(() => ({ + api_url: "https://api.invalid", + token: "token-1", + tenant_id: "tenant-1", + })), +})); + +vi.mock("openapi-fetch", () => ({ default: mocks.createClient })); +vi.mock("../../config/store", () => ({ + resolveRuntimeConfig: mocks.resolveRuntimeConfig, +})); + +describe("API client", () => { + beforeEach(() => { + vi.resetModules(); + }); + + it("creates one authenticated openapi-fetch client without a request", async () => { + const { getClient } = await import("../client"); + + expect(getClient()).toBe(getClient()); + expect(mocks.createClient).toHaveBeenCalledOnce(); + expect(mocks.createClient).toHaveBeenCalledWith({ + baseUrl: "https://api.invalid", + headers: { + Authorization: "Bearer token-1", + Accept: "application/json", + }, + }); + }); + + it("caches the configured tenant id", async () => { + const { getTenantId } = await import("../client"); + + expect(getTenantId()).toBe("tenant-1"); + expect(getTenantId()).toBe("tenant-1"); + expect(mocks.resolveRuntimeConfig).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/cli/src/api/__tests__/paths.test.ts b/apps/cli/src/api/__tests__/paths.test.ts new file mode 100644 index 000000000..0583af9cb --- /dev/null +++ b/apps/cli/src/api/__tests__/paths.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import type { ApiPaths, TaskListResponse, User } from "../paths"; + +// expectTypeOf assertions compile away at runtime, so `vitest run` alone cannot +// catch OpenAPI/type drift here. CI enforces this file via `pnpm typecheck:test` +// (tsc -p tsconfig.test.json) in .github/workflows/cli-test.yml. +describe("API path contract", () => { + it("keeps core response and path parameter types wired", () => { + expectTypeOf< + ApiPaths["/v1/auth/me"]["get"]["responses"][200]["content"]["application/json"] + >().toEqualTypeOf(); + expectTypeOf< + ApiPaths["/v1/tenants/{tenant_id}/projects/{project_id}/tasks"]["get"]["responses"][200]["content"]["application/json"] + >().toEqualTypeOf(); + expectTypeOf< + ApiPaths["/v1/tenants/{tenant_id}/projects/{project_id}/tasks/{id}"]["get"]["parameters"]["path"] + >().toEqualTypeOf<{ tenant_id: string; project_id: string; id: string }>(); + expect(true).toBe(true); + }); +}); diff --git a/apps/cli/src/commands/__tests__/commands.test.ts b/apps/cli/src/commands/__tests__/commands.test.ts new file mode 100644 index 000000000..6d391d352 --- /dev/null +++ b/apps/cli/src/commands/__tests__/commands.test.ts @@ -0,0 +1,128 @@ +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerAuthCommands } from "../auth"; +import { registerConfigCommands } from "../config"; +import { registerMyCommands } from "../my"; +import { registerProjectsCommands } from "../projects"; +import { registerSprintsCommands } from "../sprints"; +import { registerTasksCommands } from "../tasks"; + +const mocks = vi.hoisted(() => ({ + GET: vi.fn(), + POST: vi.fn(), + PUT: vi.fn(), + DELETE: vi.fn(), + loadConfigFile: vi.fn(() => ({} as Record)), + saveConfigFile: vi.fn(), + print: vi.fn(), + listProjects: vi.fn(), + resolveProject: vi.fn(), + resolveStatusId: vi.fn(), + findDoneStatusId: vi.fn(), +})); + +vi.mock("../../api/client", () => ({ + getClient: () => ({ + GET: mocks.GET, + POST: mocks.POST, + PUT: mocks.PUT, + DELETE: mocks.DELETE, + }), + getTenantId: () => "tenant-1", +})); +vi.mock("../../config/store", () => ({ + configPath: () => "/tmp/task-cli-test/config.yaml", + loadConfigFile: mocks.loadConfigFile, + saveConfigFile: mocks.saveConfigFile, +})); +vi.mock("../../utils/command", () => ({ + getOutputOptions: () => ({ json: false }), +})); +vi.mock("../../utils/output", () => ({ print: mocks.print })); +vi.mock("../../utils/projects", () => ({ + isUuid: (value: string) => value === "00000000-0000-4000-8000-000000000001", + listProjects: mocks.listProjects, + parseTaskRef: (ref: string) => { + const dash = ref.lastIndexOf("-"); + return { projectKey: ref.slice(0, dash), taskId: ref }; + }, + resolveProject: mocks.resolveProject, +})); +vi.mock("../../utils/statuses", () => ({ + resolveStatusId: mocks.resolveStatusId, + findDoneStatusId: mocks.findDoneStatusId, +})); + +function programWith(register: (program: Command) => void): Command { + const program = new Command().exitOverride().option("--json", "JSON", false); + register(program); + return program; +} + +describe("command registration and primary branches", () => { + beforeEach(() => { + mocks.GET.mockResolvedValue({ data: { id: "user-1" }, response: { status: 200 } }); + mocks.POST.mockResolvedValue({ data: { id: "task-1" }, response: { status: 201 } }); + mocks.listProjects.mockResolvedValue([{ id: "project-1", key: "APP", name: "App" }]); + mocks.resolveProject.mockResolvedValue({ id: "project-1", key: "APP", name: "App" }); + mocks.resolveStatusId.mockResolvedValue("status-1"); + }); + + it("auth parses whoami and calls the auth path", async () => { + await programWith(registerAuthCommands).parseAsync(["node", "task", "auth", "whoami"]); + expect(mocks.GET).toHaveBeenCalledWith("/v1/auth/me"); + expect(mocks.print).toHaveBeenCalledWith({ id: "user-1" }, { json: false }); + }); + + it("config parses set and persists the selected key", async () => { + await programWith(registerConfigCommands).parseAsync([ + "node", "task", "config", "set", "tenant_id", "tenant-2", + ]); + expect(mocks.saveConfigFile).toHaveBeenCalledWith({ tenant_id: "tenant-2" }); + }); + + it("my parses add and sends its title and priority", async () => { + await programWith(registerMyCommands).parseAsync([ + "node", "task", "my", "add", "Golden task", "--priority", "high", + ]); + expect(mocks.POST).toHaveBeenCalledWith( + "/v1/tenants/{tenant_id}/users/me/tasks", + expect.objectContaining({ body: { title: "Golden task", priority: "high" } }), + ); + }); + + it("projects parses list and delegates project loading", async () => { + await programWith(registerProjectsCommands).parseAsync(["node", "task", "projects", "list"]); + expect(mocks.listProjects).toHaveBeenCalledOnce(); + }); + + it("sprints parses list and builds tenant/project path parameters", async () => { + mocks.GET.mockResolvedValueOnce({ data: [], response: { status: 200 } }); + await programWith(registerSprintsCommands).parseAsync([ + "node", "task", "sprints", "list", "--project", "APP", "--status", "active", + ]); + expect(mocks.GET).toHaveBeenCalledWith( + "/v1/tenants/{tenant_id}/projects/{project_id}/sprints", + { + params: { + path: { tenant_id: "tenant-1", project_id: "project-1" }, + query: { status: "active" }, + }, + }, + ); + }); + + it("tasks parses create and resolves its optional status", async () => { + await programWith(registerTasksCommands).parseAsync([ + "node", "task", "tasks", "create", "--project", "APP", "--title", "Golden task", + "--priority", "medium", "--status", "Doing", + ]); + expect(mocks.resolveStatusId).toHaveBeenCalledWith("project-1", "Doing"); + expect(mocks.POST).toHaveBeenCalledWith( + "/v1/tenants/{tenant_id}/projects/{project_id}/tasks", + expect.objectContaining({ + body: { title: "Golden task", priority: "medium", status_id: "status-1" }, + }), + ); + }); +}); diff --git a/apps/cli/src/config/__tests__/store.test.ts b/apps/cli/src/config/__tests__/store.test.ts new file mode 100644 index 000000000..f8725ad40 --- /dev/null +++ b/apps/cli/src/config/__tests__/store.test.ts @@ -0,0 +1,65 @@ +import fs from "node:fs"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ home: "", tmpRoot: "" })); +const { realTmpdir } = vi.hoisted(() => { + // Capture the real tmpdir before vi.mock replaces node:os. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const nodeOs = require("node:os") as typeof import("node:os"); + return { realTmpdir: nodeOs.tmpdir.bind(nodeOs) }; +}); + +vi.mock("node:os", () => ({ + default: { + homedir: () => state.home, + tmpdir: () => state.tmpRoot, + }, +})); + +describe("config store", () => { + beforeEach(() => { + state.tmpRoot = realTmpdir(); + state.home = fs.mkdtempSync(path.join(state.tmpRoot, "task-cli-test-")); + vi.resetModules(); + }); + + afterEach(() => { + fs.rmSync(state.home, { recursive: true, force: true }); + vi.unstubAllEnvs(); + }); + + it("reads and writes config only below a temporary home", async () => { + const store = await import("../store"); + const expectedPath = path.join(state.home, ".config", "task", "config.yaml"); + + expect(store.configPath()).toBe(expectedPath); + expect(store.loadConfigFile()).toEqual({}); + + store.saveConfigFile({ + api_url: "https://task.invalid/", + token: "secret", + tenant_id: "tenant-1", + }); + + expect(store.loadConfigFile()).toEqual({ + api_url: "https://task.invalid/", + token: "secret", + tenant_id: "tenant-1", + }); + expect(fs.statSync(expectedPath).mode & 0o777).toBe(0o600); + }); + + it("prefers environment values and removes a trailing API slash", async () => { + vi.stubEnv("TASK_API_URL", "https://api.invalid/"); + vi.stubEnv("TASK_TOKEN", "env-token"); + vi.stubEnv("TASK_TENANT", "env-tenant"); + const { resolveRuntimeConfig } = await import("../store"); + + expect(resolveRuntimeConfig()).toEqual({ + api_url: "https://api.invalid", + token: "env-token", + tenant_id: "env-tenant", + }); + }); +}); diff --git a/apps/cli/src/utils/__tests__/utils.test.ts b/apps/cli/src/utils/__tests__/utils.test.ts new file mode 100644 index 000000000..b896049a3 --- /dev/null +++ b/apps/cli/src/utils/__tests__/utils.test.ts @@ -0,0 +1,65 @@ +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CliError, unwrapApiResult } from "../errors"; +import { getOutputOptions } from "../command"; +import { print } from "../output"; +import { isUuid, parseTaskRef } from "../projects"; +import { findDoneStatusId, resolveStatusId } from "../statuses"; + +const mocks = vi.hoisted(() => ({ GET: vi.fn() })); + +vi.mock("../../api/client", () => ({ + getClient: () => ({ GET: mocks.GET }), + getTenantId: () => "tenant-1", +})); + +describe("CLI utilities", () => { + beforeEach(() => { + mocks.GET.mockResolvedValue({ + data: [ + { id: "todo", name: "Todo", is_done_state: false }, + { id: "done", name: "Complete", is_done_state: true }, + ], + response: { status: 200 }, + }); + }); + + it("prints stable JSON and human task output", () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + print({ b: 2 }, { json: true }); + print({ seq_key: "APP-7", title: "Ship it" }, { json: false }); + expect(log).toHaveBeenNthCalledWith(1, '{\n "b": 2\n}'); + expect(log).toHaveBeenNthCalledWith(2, "APP-7\tShip it"); + }); + + it("unwraps API data and rejects an empty response", () => { + expect(unwrapApiResult({ data: 7, response: new Response() })).toBe(7); + expect(() => unwrapApiResult({ response: new Response() })).toThrow( + new CliError("API returned empty response"), + ); + }); + + it("parses UUID and KEY-number task references", () => { + const uuid = "00000000-0000-4000-8000-000000000001"; + expect(isUuid(uuid)).toBe(true); + expect(parseTaskRef(uuid)).toEqual({ uuid }); + expect(parseTaskRef("TEAM-42")).toEqual({ projectKey: "TEAM", taskId: "TEAM-42" }); + expect(() => parseTaskRef("TEAM-nope")).toThrow("Invalid task reference"); + }); + + it("finds named and done statuses through the expected API path", async () => { + await expect(resolveStatusId("project-1", "todo")).resolves.toBe("todo"); + await expect(findDoneStatusId("project-1")).resolves.toBe("done"); + expect(mocks.GET).toHaveBeenCalledWith( + "/v1/tenants/{tenant_id}/projects/{project_id}/statuses", + { params: { path: { tenant_id: "tenant-1", project_id: "project-1" } } }, + ); + }); + + it("inherits the root JSON output option", () => { + const root = new Command().option("--json", "JSON", false); + const child = root.command("child"); + root.parse(["node", "task", "--json", "child"]); + expect(getOutputOptions(child)).toEqual({ json: true }); + }); +}); diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index d6c54b132..eca41efa6 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -11,5 +11,6 @@ "declaration": true, "resolveJsonModule": true }, - "include": ["src/**/*"] -} + "include": ["src/**/*"], + "exclude": ["src/**/__tests__"] +} diff --git a/apps/cli/tsconfig.test.json b/apps/cli/tsconfig.test.json new file mode 100644 index 000000000..4b97ce48f --- /dev/null +++ b/apps/cli/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": [] +} diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts new file mode 100644 index 000000000..56fe66302 --- /dev/null +++ b/apps/cli/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/__tests__/*.test.ts"], + globalSetup: ["src/__tests__/globalSetup.ts"], + clearMocks: true, + restoreMocks: true, + }, +});