diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c500e1e..73b3e4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,9 @@ jobs: with: bun-version: latest - - run: bun install + - run: bun install --frozen-lockfile + + - run: bun run typecheck - run: bun test @@ -67,7 +69,7 @@ jobs: with: bun-version: 1.3.11 - - run: bun install + - run: bun install --frozen-lockfile - name: Validate macOS signing configuration if: ${{ matrix.sign }} diff --git a/.vscode/settings.json b/.vscode/settings.json index e847bc3..cd09337 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "source.removeUnusedImports": "explicit", "source.fixAll.biome": "explicit" }, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/bun.lock b/bun.lock index 41f30d4..155c12c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,22 +1,22 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "polar-cli", "dependencies": { - "@effect/cli": "^0.69.0", - "@effect/platform": "^0.90.2", - "@effect/platform-bun": "^0.87.1", + "@effect/platform-bun": "4.0.0-rc.112", "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", - "@polar-sh/sdk": "^0.34.11", - "effect": "^3.17.7", + "@polar-sh/sdk": "^0.47.0", + "effect": "4.0.0-rc.112", "eventsource": "^4.1.0", "open": "^10.2.0", }, "devDependencies": { "@biomejs/biome": "^2.1.4", - "@effect/language-service": "^0.35.2", + "@effect/language-service": "^0.87.2", "@sindresorhus/tsconfig": "^8.0.1", + "@types/bun": "^1.4.1", "@types/node": "^24.2.1", "tsup": "^8.5.0", "typescript": "^5.9.2", @@ -42,31 +42,11 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="], - "@effect/cli": ["@effect/cli@0.69.2", "", { "dependencies": { "ini": "^4.1.3", "toml": "^3.0.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.90.5", "@effect/printer": "^0.45.0", "@effect/printer-ansi": "^0.45.0", "effect": "^3.17.8" } }, "sha512-1xYfEzW5f6jGWDb6V6DWTsWbtdp8DgQcchbewnCwKOXnJzF5VsTcfo4lYM4TXk/RAHA1owyq7OaEmeZ+qIt1/w=="], + "@effect/language-service": ["@effect/language-service@0.87.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-CfiSoaVQO8pZgaMZGw5VvMvRGybsJ2LaSDoLYaqiVGvo/YYPYVR2GzUKY0h1NtIweq/+SQ5XLrvtzPIttWQ0GQ=="], - "@effect/cluster": ["@effect/cluster@0.56.2", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.3", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.16" } }, "sha512-BtYorswZ0MlaTqa7k+57AD80gp/TJfej85vx0gDnIxhbPQ3+xoa3EXlPrfLV7XsiAokOxE5qs/X6UoZtAVeKug=="], + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-Y5n2HhV/vsbrJls9ukX42RL7zqXQxISHegWFsP9njsCyDmGTnq7QYSO82rglwrLPwtTrv5dkSRTuGw4+oXxgMg=="], - "@effect/experimental": ["@effect/experimental@0.58.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA=="], - - "@effect/language-service": ["@effect/language-service@0.35.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg=="], - - "@effect/platform": ["@effect/platform@0.90.10", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.17.13" } }, "sha512-QhDPgCaLfIMQKOCoCPQvRUS+Y34iYJ07jdZ/CBAvYFvg/iUBebsmFuHL63RCD/YZH9BuK/kqqLYAA3M0fmUEgg=="], - - "@effect/platform-bun": ["@effect/platform-bun@0.87.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-I88d0YqWbvLY2GGeIxK3r5k0l/MoUCCnxiHJG+X6gqaHu+pIs0djDtJ+ORhw/3qha9ojcVu6pyaBmnUjgzQHWQ=="], - - "@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="], - - "@effect/printer": ["@effect/printer@0.45.0", "", { "peerDependencies": { "@effect/typeclass": "^0.36.0", "effect": "^3.17.0" } }, "sha512-UpFBH2JKAgakSWpue6yKkIAXMq+3md/CPb9s/NGl28vDu1P33cvDeeDL/1EOzFk8WqhIs3oKwPMDnd3jUhjzdg=="], - - "@effect/printer-ansi": ["@effect/printer-ansi@0.45.0", "", { "dependencies": { "@effect/printer": "^0.45.0" }, "peerDependencies": { "@effect/typeclass": "^0.36.0", "effect": "^3.17.0" } }, "sha512-3MS02RP83eZaBJX98PRI4f5kyoEVyNfg2Qu/XUWQMFRp4wvmgNwEy18RjO9G6s7uB8NaYXTpQVDmtUoKARx7fA=="], - - "@effect/rpc": ["@effect/rpc@0.73.0", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-iMPf6tTriz8sK0l5x4koFId8Hz5nFptHYg8WqyjHGIIVLTpZxuiSqhmXZG7FnAs5N2n6uCEws4wWGcIgXNUrFg=="], - - "@effect/sql": ["@effect/sql@0.49.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA=="], - - "@effect/typeclass": ["@effect/typeclass@0.36.0", "", { "peerDependencies": { "effect": "^3.17.0" } }, "sha512-+8xYvX4tjD7gKwGYzOyFh90I+ptdXzoNHLQTSa8kGh/xOVZMIGYb0VgLoNHE02UsuVrB+JJJuBmKLdd5TeDTPg=="], - - "@effect/workflow": ["@effect/workflow@0.16.0", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "@effect/rpc": "^0.73.0", "effect": "^3.19.13" } }, "sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -130,47 +110,19 @@ "@lemonsqueezy/lemonsqueezy.js": ["@lemonsqueezy/lemonsqueezy.js@4.0.0", "", {}, "sha512-xcY1/lDrY7CpIF98WKiL1ElsfoVhddP7FT0fw7ssOzrFqQsr44HgolKrQZxd9SywsCPn12OTOUieqDIokI3mFg=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - - "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], - - "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], - - "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], - - "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], - - "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], - - "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], - - "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], - - "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], - - "@polar-sh/sdk": ["@polar-sh/sdk@0.34.17", "", { "dependencies": { "standardwebhooks": "^1.0.0", "zod": "^3.25.76" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.5.0 <1.10.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"], "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-+eJAAyyP4CAtMy9Hd6gaNXErjaH3KuTXJFv72kqlCCvv7SweBlM4U2+zpeYAZvd/YMRZq/c447f0a0DD2e7UEA=="], + "@polar-sh/sdk": ["@polar-sh/sdk@0.47.1", "", { "dependencies": { "standardwebhooks": "^1.0.0", "zod": "^3.25.65 || ^4.0.0" } }, "sha512-fkz7wPLbqfuDmY9LxuXpE2uP2TAV6J0q/YN5hJ4UBxpjbkB0hKM6c4R35N89t83dfzMlG6EOlqOn+Rd1T6XrJQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], @@ -226,16 +178,20 @@ "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/node": ["@types/node@24.10.11", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], @@ -260,7 +216,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "effect": ["effect@3.19.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ=="], + "effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], @@ -268,34 +224,24 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -308,16 +254,12 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + "msgpackr": ["msgpackr@2.1.0", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ=="], - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], "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=="], - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -336,7 +278,7 @@ "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=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -360,8 +302,6 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -374,14 +314,10 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], } } diff --git a/package.json b/package.json index 19282c9..2c2711c 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "bin": "bin/cli.js", "type": "module", "scripts": { + "prepare": "effect-language-service patch", "build": "tsup ./src/cli.ts --format esm --outDir bin", "dev": "tsc --watch", + "typecheck": "tsc --noEmit", "test": "bun test", "check": "biome check --write ./src", "build:binary": "bun build ./src/cli.ts --compile --outfile polar", @@ -23,19 +25,18 @@ "license": "Apache-2.0", "packageManager": "pnpm@10.5.2", "dependencies": { - "@effect/cli": "^0.69.0", - "@effect/platform": "^0.90.2", - "@effect/platform-bun": "^0.87.1", + "@effect/platform-bun": "4.0.0-rc.112", "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", "@polar-sh/sdk": "^0.47.0", - "effect": "^3.17.7", + "effect": "4.0.0-rc.112", "eventsource": "^4.1.0", "open": "^10.2.0" }, "devDependencies": { "@biomejs/biome": "^2.1.4", - "@effect/language-service": "^0.35.2", + "@effect/language-service": "^0.87.2", "@sindresorhus/tsconfig": "^8.0.1", + "@types/bun": "^1.4.1", "@types/node": "^24.2.1", "tsup": "^8.5.0", "typescript": "^5.9.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b0ea36..d141a3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,9 @@ importers: .: dependencies: - '@effect/cli': - specifier: ^0.69.0 - version: 0.69.2(@effect/platform@0.90.10(effect@3.19.19))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) - '@effect/platform': - specifier: ^0.90.2 - version: 0.90.10(effect@3.19.19) '@effect/platform-bun': - specifier: ^0.87.1 - version: 0.87.1(@effect/cluster@0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112(effect@4.0.0-rc.112) '@lemonsqueezy/lemonsqueezy.js': specifier: ^4.0.0 version: 4.0.0 @@ -24,8 +18,8 @@ importers: specifier: ^0.47.0 version: 0.47.0 effect: - specifier: ^3.17.7 - version: 3.19.19 + specifier: 4.0.0-rc.112 + version: 4.0.0-rc.112 eventsource: specifier: ^4.1.0 version: 4.1.0 @@ -37,11 +31,14 @@ importers: specifier: ^2.1.4 version: 2.4.4 '@effect/language-service': - specifier: ^0.35.2 - version: 0.35.2 + specifier: ^0.87.2 + version: 0.87.2 '@sindresorhus/tsconfig': specifier: ^8.0.1 version: 8.1.0 + '@types/bun': + specifier: ^1.4.1 + version: 1.4.1 '@types/node': specifier: ^24.2.1 version: 24.10.13 @@ -107,100 +104,20 @@ packages: cpu: [x64] os: [win32] - '@effect/cli@0.69.2': - resolution: {integrity: sha512-1xYfEzW5f6jGWDb6V6DWTsWbtdp8DgQcchbewnCwKOXnJzF5VsTcfo4lYM4TXk/RAHA1owyq7OaEmeZ+qIt1/w==} - peerDependencies: - '@effect/platform': ^0.90.5 - '@effect/printer': ^0.45.0 - '@effect/printer-ansi': ^0.45.0 - effect: ^3.17.8 - - '@effect/cluster@0.56.4': - resolution: {integrity: sha512-7Je5/JlbZOlsSxsbKjr97dJed2cNGWsb+TLNgMcr5mRDbcWlFOTUGvsrisEJV6waosYLIg+2omPdvnvRoYKdhA==} - peerDependencies: - '@effect/platform': ^0.94.5 - '@effect/rpc': ^0.73.1 - '@effect/sql': ^0.49.0 - '@effect/workflow': ^0.16.0 - effect: ^3.19.17 - - '@effect/experimental@0.58.0': - resolution: {integrity: sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA==} - peerDependencies: - '@effect/platform': ^0.94.0 - effect: ^3.19.13 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/language-service@0.35.2': - resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} + '@effect/language-service@0.87.2': + resolution: {integrity: sha512-CfiSoaVQO8pZgaMZGw5VvMvRGybsJ2LaSDoLYaqiVGvo/YYPYVR2GzUKY0h1NtIweq/+SQ5XLrvtzPIttWQ0GQ==} hasBin: true - '@effect/platform-bun@0.87.1': - resolution: {integrity: sha512-I88d0YqWbvLY2GGeIxK3r5k0l/MoUCCnxiHJG+X6gqaHu+pIs0djDtJ+ORhw/3qha9ojcVu6pyaBmnUjgzQHWQ==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform-node-shared@0.57.1': - resolution: {integrity: sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform@0.90.10': - resolution: {integrity: sha512-QhDPgCaLfIMQKOCoCPQvRUS+Y34iYJ07jdZ/CBAvYFvg/iUBebsmFuHL63RCD/YZH9BuK/kqqLYAA3M0fmUEgg==} + '@effect/platform-bun@4.0.0-rc.112': + resolution: {integrity: sha512-Y5n2HhV/vsbrJls9ukX42RL7zqXQxISHegWFsP9njsCyDmGTnq7QYSO82rglwrLPwtTrv5dkSRTuGw4+oXxgMg==} peerDependencies: - effect: ^3.17.13 + effect: ^4.0.0-rc.112 - '@effect/printer-ansi@0.45.0': - resolution: {integrity: sha512-3MS02RP83eZaBJX98PRI4f5kyoEVyNfg2Qu/XUWQMFRp4wvmgNwEy18RjO9G6s7uB8NaYXTpQVDmtUoKARx7fA==} - peerDependencies: - '@effect/typeclass': ^0.36.0 - effect: ^3.17.0 - - '@effect/printer@0.45.0': - resolution: {integrity: sha512-UpFBH2JKAgakSWpue6yKkIAXMq+3md/CPb9s/NGl28vDu1P33cvDeeDL/1EOzFk8WqhIs3oKwPMDnd3jUhjzdg==} - peerDependencies: - '@effect/typeclass': ^0.36.0 - effect: ^3.17.0 - - '@effect/rpc@0.73.2': - resolution: {integrity: sha512-td7LHDgBOYKg+VgGWEelD8rSAmvjXz7am17vfxZROX5qIYuvH7drL/z4p5xQFadhHZ7DYdlFpqdO9ggc77OCIw==} - peerDependencies: - '@effect/platform': ^0.94.5 - effect: ^3.19.18 - - '@effect/sql@0.49.0': - resolution: {integrity: sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA==} - peerDependencies: - '@effect/experimental': ^0.58.0 - '@effect/platform': ^0.94.0 - effect: ^3.19.13 - - '@effect/typeclass@0.36.0': - resolution: {integrity: sha512-+8xYvX4tjD7gKwGYzOyFh90I+ptdXzoNHLQTSa8kGh/xOVZMIGYb0VgLoNHE02UsuVrB+JJJuBmKLdd5TeDTPg==} - peerDependencies: - effect: ^3.17.0 - - '@effect/workflow@0.16.0': - resolution: {integrity: sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw==} + '@effect/platform-node-shared@4.0.0-rc.112': + resolution: {integrity: sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ==} + engines: {node: '>=18.0.0'} peerDependencies: - '@effect/experimental': ^0.58.0 - '@effect/platform': ^0.94.0 - '@effect/rpc': ^0.73.0 - effect: ^3.19.13 + effect: ^4.0.0-rc.112 '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} @@ -375,118 +292,36 @@ packages: resolution: {integrity: sha512-xcY1/lDrY7CpIF98WKiL1ElsfoVhddP7FT0fw7ssOzrFqQsr44HgolKrQZxd9SywsCPn12OTOUieqDIokI3mFg==} engines: {node: '>=20'} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} cpu: [x64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} cpu: [arm64] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} cpu: [arm] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} cpu: [x64] os: [linux] - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} cpu: [x64] os: [win32] - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - '@polar-sh/sdk@0.47.0': resolution: {integrity: sha512-tZEt8eLWsQVhmyfUQzXhnN84e5Cnu/JsYFnCci1ranWkIGveQc+wTiyzDCG9Tl1xiz1Y/ztPsAJ1fevj6ExdAA==} @@ -622,8 +457,8 @@ packages: '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/bun@1.4.1': + resolution: {integrity: sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -631,6 +466,9 @@ packages: '@types/node@24.10.13': resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -639,6 +477,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + bun-types@1.4.1: + resolution: {integrity: sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -693,8 +534,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - effect@3.19.19: - resolution: {integrity: sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg==} + effect@4.0.0-rc.112: + resolution: {integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==} esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} @@ -709,9 +550,9 @@ packages: resolution: {integrity: sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==} engines: {node: '>=20.0.0'} - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -725,9 +566,6 @@ packages: picomatch: optional: true - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -736,23 +574,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -766,9 +592,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -789,22 +612,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@1.11.8: - resolution: {integrity: sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==} - - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + msgpackr@2.1.0: + resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -852,8 +669,8 @@ packages: yaml: optional: true - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} @@ -898,9 +715,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -938,12 +752,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1003,102 +813,25 @@ snapshots: '@biomejs/cli-win32-x64@2.4.4': optional: true - '@effect/cli@0.69.2(@effect/platform@0.90.10(effect@3.19.19))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/platform': 0.90.10(effect@3.19.19) - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19) - '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19) - effect: 3.19.19 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.2 + '@effect/language-service@0.87.2': {} - '@effect/cluster@0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/platform': 0.90.10(effect@3.19.19) - '@effect/rpc': 0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/workflow': 0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) - effect: 3.19.19 - kubernetes-types: 1.30.0 - - '@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19)': + '@effect/platform-bun@4.0.0-rc.112(effect@4.0.0-rc.112)': dependencies: - '@effect/platform': 0.90.10(effect@3.19.19) - effect: 3.19.19 - uuid: 11.1.0 - - '@effect/language-service@0.35.2': {} - - '@effect/platform-bun@0.87.1(@effect/cluster@0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/cluster': 0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) - '@effect/platform': 0.90.10(effect@3.19.19) - '@effect/platform-node-shared': 0.57.1(@effect/cluster@0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) - '@effect/rpc': 0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - effect: 3.19.19 - multipasta: 0.2.7 + '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) + effect: 4.0.0-rc.112 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node-shared@0.57.1(@effect/cluster@0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19)': + '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': dependencies: - '@effect/cluster': 0.56.4(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) - '@effect/platform': 0.90.10(effect@3.19.19) - '@effect/rpc': 0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@parcel/watcher': 2.5.6 - effect: 3.19.19 - multipasta: 0.2.7 - ws: 8.19.0 + '@types/ws': 8.18.1 + effect: 4.0.0-rc.112 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform@0.90.10(effect@3.19.19)': - dependencies: - effect: 3.19.19 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.8 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19) - '@effect/typeclass': 0.36.0(effect@3.19.19) - effect: 3.19.19 - - '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/typeclass': 0.36.0(effect@3.19.19) - effect: 3.19.19 - - '@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/platform': 0.90.10(effect@3.19.19) - effect: 3.19.19 - msgpackr: 1.11.8 - - '@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/experimental': 0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/platform': 0.90.10(effect@3.19.19) - effect: 3.19.19 - uuid: 11.1.0 - - '@effect/typeclass@0.36.0(effect@3.19.19)': - dependencies: - effect: 3.19.19 - - '@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.90.10(effect@3.19.19))(@effect/rpc@0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19))(effect@3.19.19)': - dependencies: - '@effect/experimental': 0.58.0(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - '@effect/platform': 0.90.10(effect@3.19.19) - '@effect/rpc': 0.73.2(@effect/platform@0.90.10(effect@3.19.19))(effect@3.19.19) - effect: 3.19.19 - '@esbuild/aix-ppc64@0.27.3': optional: true @@ -1193,84 +926,24 @@ snapshots: '@lemonsqueezy/lemonsqueezy.js@4.0.0': {} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true - '@parcel/watcher-darwin-x64@2.5.6': + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': optional: true - '@parcel/watcher-freebsd-x64@2.5.6': + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.6': + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': optional: true - '@parcel/watcher-linux-arm-musl@2.5.6': + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': optional: true - '@parcel/watcher-linux-arm64-glibc@2.5.6': + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - '@polar-sh/sdk@0.47.0': dependencies: standardwebhooks: 1.0.0 @@ -1355,7 +1028,9 @@ snapshots: '@stablelib/base64@1.0.1': {} - '@standard-schema/spec@1.1.0': {} + '@types/bun@1.4.1': + dependencies: + bun-types: 1.4.1 '@types/estree@1.0.8': {} @@ -1363,10 +1038,18 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.13 + acorn@8.16.0: {} any-promise@1.3.0: {} + bun-types@1.4.1: + dependencies: + '@types/node': 24.10.13 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -1401,12 +1084,13 @@ snapshots: define-lazy-prop@3.0.0: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true - effect@3.19.19: + effect@4.0.0-rc.112: dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 + fast-check: 4.9.0 + msgpackr: 2.1.0 esbuild@0.27.3: optionalDependencies: @@ -1443,9 +1127,9 @@ snapshots: dependencies: eventsource-parser: 3.0.6 - fast-check@3.23.2: + fast-check@4.9.0: dependencies: - pure-rand: 6.1.0 + pure-rand: 8.4.2 fast-sha256@1.3.0: {} @@ -1453,8 +1137,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - find-my-way-ts@0.1.6: {} - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -1464,16 +1146,8 @@ snapshots: fsevents@2.3.3: optional: true - ini@4.1.3: {} - is-docker@3.0.0: {} - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -1484,8 +1158,6 @@ snapshots: joycon@3.1.1: {} - kubernetes-types@1.30.0: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -1505,23 +1177,21 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.3: + msgpackr-extract@3.0.4: dependencies: node-gyp-build-optional-packages: 5.2.2 optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@1.11.8: + msgpackr@2.1.0: optionalDependencies: - msgpackr-extract: 3.0.3 - - multipasta@0.2.7: {} + msgpackr-extract: 3.0.4 mz@2.7.0: dependencies: @@ -1529,8 +1199,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - node-addon-api@7.1.1: {} - node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -1565,7 +1233,7 @@ snapshots: optionalDependencies: yaml: 2.8.2 - pure-rand@6.1.0: {} + pure-rand@8.4.2: {} readdirp@4.1.2: {} @@ -1636,8 +1304,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - toml@3.0.0: {} - tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -1675,14 +1341,13 @@ snapshots: undici-types@7.16.0: {} - uuid@11.1.0: {} - - ws@8.19.0: {} + ws@8.21.3: {} wsl-utils@0.1.0: dependencies: is-wsl: 3.1.1 - yaml@2.8.2: {} + yaml@2.8.2: + optional: true zod@4.3.6: {} diff --git a/src/cli.ts b/src/cli.ts index 2776c23..24453a8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ -import { Command } from "@effect/cli"; -import { BunContext, BunRuntime } from "@effect/platform-bun"; +import { BunRuntime, BunServices } from "@effect/platform-bun"; import { Effect, Layer } from "effect"; +import { Command } from "effect/unstable/cli"; import { listen } from "./commands/listen"; import { login } from "./commands/login"; import { logout } from "./commands/logout"; @@ -9,32 +9,28 @@ import { update } from "./commands/update"; import * as Migration from "./services/migration/migrate"; import * as OAuth from "./services/oauth"; import * as Polar from "./services/polar"; -import { showUpdateNotice, checkForUpdateInBackground } from "./services/update-check"; +import { + checkForUpdateInBackground, + showUpdateNotice, +} from "./services/update-check"; import { VERSION } from "./version"; const mainCommand = Command.make("polar").pipe( - Command.withSubcommands([ - login, - logout, - migrate, - listen, - update - ]) + Command.withSubcommands([login, logout, migrate, listen, update]), ); const cli = Command.run(mainCommand, { - name: "Polar CLI", - version: VERSION, + version: VERSION.replace(/^v/, ""), }); const services = Layer.mergeAll( - OAuth.layer, - Polar.layer, - Migration.layer, - BunContext.layer + OAuth.layer, + Polar.layer, + Migration.layer, + BunServices.layer, ); showUpdateNotice(); checkForUpdateInBackground(); -cli(process.argv).pipe(Effect.provide(services), BunRuntime.runMain); +cli.pipe(Effect.provide(services), BunRuntime.runMain); diff --git a/src/commands/listen.test.ts b/src/commands/listen.test.ts index 8b10ebd..055ed28 100644 --- a/src/commands/listen.test.ts +++ b/src/commands/listen.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, mock, test } from "bun:test"; import { Effect, Fiber } from "effect"; import { - type CreateEventSource, - type ListenEventSource, - startListening, + type CreateEventSource, + type ListenEventSource, + startListening, } from "./listen"; /** @@ -11,200 +11,233 @@ import { * test drive the stream by pushing raw message payloads into `onmessage`. */ class FakeEventSource implements ListenEventSource { - static instances: FakeEventSource[] = []; - - url: string; - init: { fetch: typeof fetch }; - onmessage: ((event: MessageEvent) => void) | null = null; - onerror: ((event: { code?: number; message?: string }) => void) | null = - null; - closed = false; - - constructor(url: string, init: { fetch: typeof fetch }) { - this.url = url; - this.init = init; - FakeEventSource.instances.push(this); - } - - close() { - this.closed = true; - } - - /** Simulate the server pushing a message frame. */ - emit(data: unknown) { - this.onmessage?.({ data: JSON.stringify(data) } as MessageEvent); - } + static instances: FakeEventSource[] = []; + + url: string; + init: { fetch: typeof fetch }; + onmessage: ((event: MessageEvent) => void) | null = null; + onerror: + | ((event: { + code?: number | undefined; + message?: string | undefined; + }) => void) + | null = null; + closed = false; + + constructor(url: string, init: { fetch: typeof fetch }) { + this.url = url; + this.init = init; + FakeEventSource.instances.push(this); + } + + close() { + this.closed = true; + } + + /** Simulate the server pushing a message frame. */ + emit(data: unknown) { + this.onmessage?.({ data: JSON.stringify(data) } as MessageEvent); + } } /** Returns the event source at `index`, asserting it exists. */ const instanceAt = (index: number): FakeEventSource => { - const instance = FakeEventSource.instances[index]; - if (!instance) { - throw new Error(`No event source created at index ${index}`); - } - return instance; + const instance = FakeEventSource.instances[index]; + if (!instance) { + throw new Error(`No event source created at index ${index}`); + } + return instance; }; const createFakeEventSource: CreateEventSource = (url, init) => - new FakeEventSource(url, init as { fetch: typeof fetch }); + new FakeEventSource(url, init as { fetch: typeof fetch }); const okResponse = () => - Promise.resolve({ status: 200, statusText: "OK" } as Response); - -const baseOptions = (overrides: Partial[0]>) => ({ - listenUrl: "https://api.polar.sh/v1/cli/listen/org_123", - forwardUrl: "http://localhost:3000/webhook", - organizationName: "Acme", - accessToken: "test-token", - createEventSource: createFakeEventSource, - forward: mock(okResponse) as unknown as typeof fetch, - ...overrides, + Promise.resolve({ status: 200, statusText: "OK" } as Response); + +const baseOptions = ( + overrides: Partial[0]>, +) => ({ + listenUrl: "https://api.polar.sh/v1/cli/listen/org_123", + forwardUrl: "http://localhost:3000/webhook", + organizationName: "Acme", + accessToken: "test-token", + createEventSource: createFakeEventSource, + forward: mock(okResponse) as unknown as typeof fetch, + ...overrides, }); -const fibers: Fiber.RuntimeFiber[] = []; +const fibers: Fiber.Fiber[] = []; /** Start the stream in the background and return the latest fake event source. */ const run = (overrides: Partial[0]> = {}) => { - const options = baseOptions(overrides); - const fiber = Effect.runFork(startListening(options)); - fibers.push(fiber); - return { options, fiber }; + const options = baseOptions(overrides); + const fiber = Effect.runFork(startListening(options)); + fibers.push(fiber); + return { options, fiber }; }; afterEach(async () => { - await Promise.all(fibers.map((fiber) => Effect.runPromise(Fiber.interrupt(fiber)))); - fibers.length = 0; - FakeEventSource.instances = []; + await Promise.all( + fibers.map((fiber) => Effect.runPromise(Fiber.interrupt(fiber))), + ); + fibers.length = 0; + FakeEventSource.instances = []; }); describe("startListening", () => { - test("opens a single event source for the listen url", () => { - run(); - - expect(FakeEventSource.instances).toHaveLength(1); - expect(instanceAt(0).url).toBe( - "https://api.polar.sh/v1/cli/listen/org_123", - ); - }); - - test("authenticates the stream with a bearer token while preserving headers", async () => { - const forward = mock(okResponse) as unknown as typeof fetch; - run({ forward }); - - const eventSource = instanceAt(0); - await eventSource.init.fetch("https://api.polar.sh/v1/cli/listen/org_123", { - headers: { Accept: "text/event-stream" }, - } as RequestInit); - - expect(forward).toHaveBeenCalledTimes(1); - expect(forward).toHaveBeenCalledWith( - "https://api.polar.sh/v1/cli/listen/org_123", - expect.objectContaining({ - headers: expect.objectContaining({ - Accept: "text/event-stream", - Authorization: "Bearer test-token", - }), - }), - ); - }); - - test("forwards decoded webhook events to the local url", async () => { - const forward = mock(okResponse) as unknown as typeof fetch; - run({ forward }); - - // The server sends `payload.payload` as the exact raw JSON string it - // signed, not a parsed object. The client must forward it byte-for-byte. - const rawPayload = JSON.stringify({ - type: "order.created", - timestamp: "2026-01-01T00:00:00Z", - data: {}, - }); - - instanceAt(0).emit({ - id: "evt_1", - key: "webhook", - payload: { webhook_event_id: "whid_1", payload: rawPayload }, - headers: { - "user-agent": "polar.sh webhooks", - "content-type": "application/json", - "webhook-id": "wh_1", - "webhook-timestamp": "12345", - "webhook-signature": "sig", - }, - }); - - expect(forward).toHaveBeenCalledWith( - "http://localhost:3000/webhook", - expect.objectContaining({ - method: "POST", - body: rawPayload, - }), - ); - }); - - test("reconnects when the server sends a reconnect event", () => { - run(); - - expect(FakeEventSource.instances).toHaveLength(1); - const first = instanceAt(0); - - first.emit({ type: "reconnect" }); - - // The original connection is torn down and a brand new one is opened. - expect(first.closed).toBe(true); - expect(FakeEventSource.instances).toHaveLength(2); - expect(instanceAt(1).url).toBe(first.url); - expect(instanceAt(1).closed).toBe(false); - }); - - test("does not reconnect on the connected acknowledgement", () => { - run(); - - const first = instanceAt(0); - first.emit({ - key: "connected", - ts: "2026-01-01T00:00:00Z", - secret: "whsec_test", - }); - - expect(first.closed).toBe(false); - expect(FakeEventSource.instances).toHaveLength(1); - }); - - test("only prints the connection banner for the first connection", () => { - const logged: string[] = []; - const log = console.log; - console.log = (...args: unknown[]) => { - logged.push(args.join(" ")); - }; - - try { - run(); - - const ack = { - key: "connected", - ts: "2026-01-01T00:00:00Z", - secret: "whsec_test", - }; - - // First connection acknowledges and prints the banner. - instanceAt(0).emit(ack); - const firstBannerCount = logged.filter((line) => - line.includes("Connected"), - ).length; - expect(firstBannerCount).toBe(1); - - // Server asks us to reconnect; the fresh connection acknowledges again. - instanceAt(0).emit({ type: "reconnect" }); - instanceAt(1).emit(ack); - - // The banner is not repeated for the reconnected stream. - const totalBannerCount = logged.filter((line) => - line.includes("Connected"), - ).length; - expect(totalBannerCount).toBe(1); - } finally { - console.log = log; - } - }); + test.each([ + 401, 500, + ])("preserves stream error code %i as a typed ListenError", async (code) => { + const { fiber } = run(); + const source = instanceAt(0); + const cause = { code, message: "Stream failed (request 401)" }; + source.onerror?.(cause); + + const error = await Effect.runPromise(Fiber.join(fiber).pipe(Effect.flip)); + expect(error).toMatchObject({ + _tag: "ListenError", + code, + message: cause.message, + cause, + }); + expect(source.closed).toBe(true); + }); + + test("keeps the stream open for errors without a code", () => { + run(); + const source = instanceAt(0); + source.onerror?.({ message: "Transient connection error" }); + expect(source.closed).toBe(false); + }); + + test("opens a single event source for the listen url", () => { + run(); + + expect(FakeEventSource.instances).toHaveLength(1); + expect(instanceAt(0).url).toBe( + "https://api.polar.sh/v1/cli/listen/org_123", + ); + }); + + test("authenticates the stream with a bearer token while preserving headers", async () => { + const forward = mock(okResponse) as unknown as typeof fetch; + run({ forward }); + + const eventSource = instanceAt(0); + await eventSource.init.fetch("https://api.polar.sh/v1/cli/listen/org_123", { + headers: { Accept: "text/event-stream" }, + } as RequestInit); + + expect(forward).toHaveBeenCalledTimes(1); + expect(forward).toHaveBeenCalledWith( + "https://api.polar.sh/v1/cli/listen/org_123", + expect.objectContaining({ + headers: expect.objectContaining({ + Accept: "text/event-stream", + Authorization: "Bearer test-token", + }), + }), + ); + }); + + test("forwards decoded webhook events to the local url", async () => { + const forward = mock(okResponse) as unknown as typeof fetch; + run({ forward }); + + // The server sends `payload.payload` as the exact raw JSON string it + // signed, not a parsed object. The client must forward it byte-for-byte. + const rawPayload = JSON.stringify({ + type: "order.created", + timestamp: "2026-01-01T00:00:00Z", + data: {}, + }); + + instanceAt(0).emit({ + id: "evt_1", + key: "webhook", + payload: { webhook_event_id: "whid_1", payload: rawPayload }, + headers: { + "user-agent": "polar.sh webhooks", + "content-type": "application/json", + "webhook-id": "wh_1", + "webhook-timestamp": "12345", + "webhook-signature": "sig", + }, + }); + + expect(forward).toHaveBeenCalledWith( + "http://localhost:3000/webhook", + expect.objectContaining({ + method: "POST", + body: rawPayload, + }), + ); + }); + + test("reconnects when the server sends a reconnect event", () => { + run(); + + expect(FakeEventSource.instances).toHaveLength(1); + const first = instanceAt(0); + + first.emit({ type: "reconnect" }); + + // The original connection is torn down and a brand new one is opened. + expect(first.closed).toBe(true); + expect(FakeEventSource.instances).toHaveLength(2); + expect(instanceAt(1).url).toBe(first.url); + expect(instanceAt(1).closed).toBe(false); + }); + + test("does not reconnect on the connected acknowledgement", () => { + run(); + + const first = instanceAt(0); + first.emit({ + key: "connected", + ts: "2026-01-01T00:00:00Z", + secret: "whsec_test", + }); + + expect(first.closed).toBe(false); + expect(FakeEventSource.instances).toHaveLength(1); + }); + + test("only prints the connection banner for the first connection", () => { + const logged: string[] = []; + const log = console.log; + console.log = (...args: unknown[]) => { + logged.push(args.join(" ")); + }; + + try { + run(); + + const ack = { + key: "connected", + ts: "2026-01-01T00:00:00Z", + secret: "whsec_test", + }; + + // First connection acknowledges and prints the banner. + instanceAt(0).emit(ack); + const firstBannerCount = logged.filter((line) => + line.includes("Connected"), + ).length; + expect(firstBannerCount).toBe(1); + + // Server asks us to reconnect; the fresh connection acknowledges again. + instanceAt(0).emit({ type: "reconnect" }); + instanceAt(1).emit(ack); + + // The banner is not repeated for the reconnected stream. + const totalBannerCount = logged.filter((line) => + line.includes("Connected"), + ).length; + expect(totalBannerCount).toBe(1); + } finally { + console.log = log; + } + }); }); diff --git a/src/commands/listen.ts b/src/commands/listen.ts index ba9328a..6bb98ff 100644 --- a/src/commands/listen.ts +++ b/src/commands/listen.ts @@ -1,60 +1,66 @@ -import { Args, Command } from "@effect/cli"; -import { Effect, Either, Redacted, Schema } from "effect"; +import { Data, Effect, Exit, Redacted, Schema } from "effect"; +import { Argument, Command } from "effect/unstable/cli"; import { - type ErrorEvent, - EventSource, - type EventSourceInit, + type ErrorEvent, + EventSource, + type EventSourceInit, } from "eventsource"; import { environmentPrompt } from "../prompts/environment"; import { organizationLoginPrompt } from "../prompts/organizations"; import { - ListenAck, - ListenReconnect, - ListenWebhookEvent, + ListenAck, + ListenReconnect, + ListenWebhookEvent, } from "../schemas/Events"; import type { Token } from "../schemas/Tokens"; import * as OAuth from "../services/oauth"; export const LISTEN_BASE_URLS = { - production: "https://api.polar.sh/v1/cli/listen", - sandbox: "https://sandbox-api.polar.sh/v1/cli/listen", + production: "https://api.polar.sh/v1/cli/listen", + sandbox: "https://sandbox-api.polar.sh/v1/cli/listen", } as const; -const url = Args.text({ name: "url" }); +export class ListenError extends Data.TaggedError("ListenError")<{ + message: string; + code: number; + cause?: unknown; +}> {} + +const url = Argument.string("url"); /** * Minimal subset of the {@link EventSource} interface that {@link startListening} * relies on. Keeping it narrow lets tests substitute a fake implementation. */ export type ListenEventSource = Pick & { - onmessage: ((event: MessageEvent) => void) | null; - onerror: ((event: ErrorEvent) => void) | null; + onmessage: ((event: MessageEvent) => void) | null; + onerror: ((event: ErrorEvent) => void) | null; }; export type CreateEventSource = ( - url: string, - init: EventSourceInit, + url: string, + init: EventSourceInit, ) => ListenEventSource; export type Forward = typeof fetch; export interface StartListeningOptions { - /** Fully qualified URL of the CLI listen stream. */ - listenUrl: string; - /** Local URL incoming webhook events are forwarded to. */ - forwardUrl: string; - /** Name of the organization, used for display only. */ - organizationName: string; - /** Access token used to authenticate the stream. */ - accessToken: string; - /** Factory for the event source. Overridable for testing. */ - createEventSource?: CreateEventSource; - /** `fetch` implementation used to forward webhook events. Overridable for testing. */ - forward?: Forward; + /** Fully qualified URL of the CLI listen stream. */ + listenUrl: string; + /** Local URL incoming webhook events are forwarded to. */ + forwardUrl: string; + /** Name of the organization, used for display only. */ + organizationName: string; + /** Access token used to authenticate the stream. */ + accessToken: string; + /** Factory for the event source. Overridable for testing. */ + createEventSource?: CreateEventSource; + /** `fetch` implementation used to forward webhook events. Overridable for testing. */ + forward?: Forward; } const defaultCreateEventSource: CreateEventSource = (listenUrl, init) => - new EventSource(listenUrl, init); + new EventSource(listenUrl, init); /** * Opens the CLI listen stream and forwards incoming webhook events to the local @@ -63,173 +69,166 @@ const defaultCreateEventSource: CreateEventSource = (listenUrl, init) => * the stream errors out. */ export const startListening = ({ - listenUrl, - forwardUrl, - organizationName, - accessToken, - createEventSource = defaultCreateEventSource, - forward = fetch, + listenUrl, + forwardUrl, + organizationName, + accessToken, + createEventSource = defaultCreateEventSource, + forward = fetch, }: StartListeningOptions) => - Effect.async((resume) => { - let eventSource: ListenEventSource; - // Reconnections happen transparently behind the scenes; the connection - // banner should only ever be shown for the very first connection so the - // output reads as one continuous stream. - let bannerShown = false; - - const connect = () => { - eventSource = createEventSource(listenUrl, { - fetch: (input, init) => - forward(input, { - ...init, - headers: { - ...init.headers, - Authorization: `Bearer ${accessToken}`, - }, - }), - }); - - eventSource.onmessage = (event) => { - const json = JSON.parse(event.data); - const ack = Schema.decodeUnknownEither(ListenAck)(json); - - if (Either.isRight(ack)) { - if (bannerShown) { - return; - } - bannerShown = true; - - const { secret } = ack.right; - const dim = "\x1b[2m"; - const bold = "\x1b[1m"; - const cyan = "\x1b[36m"; - const reset = "\x1b[0m"; - - console.log(""); - console.log( - ` ${bold}${cyan}Connected${reset} ${bold}${organizationName}${reset}`, - ); - console.log(` ${dim}Secret${reset} ${secret}`); - console.log(` ${dim}Forwarding${reset} ${forwardUrl}`); - console.log(""); - console.log(` ${dim}Waiting for events...${reset}`); - console.log(""); - - return; - } - - const reconnect = Schema.decodeUnknownEither(ListenReconnect)(json); - - if (Either.isRight(reconnect)) { - // The server is about to drop the connection; tear down the current - // stream and immediately establish a new one. - eventSource.close(); - connect(); - return; - } - - const webhookEvent = - Schema.decodeUnknownEither(ListenWebhookEvent)(json); - - if (Either.isLeft(webhookEvent)) { - console.error(">> Failed to decode event"); - return; - } - - const rawPayload = webhookEvent.right.payload.payload; - - const eventType = (() => { - try { - const parsed = JSON.parse(rawPayload) as { type?: string }; - return parsed.type ?? "event"; - } catch { - return "event"; - } - })(); - - forward(forwardUrl, { - method: "POST", - headers: webhookEvent.right.headers, - body: rawPayload, - }) - .then((res) => { - const cyan = "\x1b[36m"; - const reset = "\x1b[0m"; - console.log( - `>> '${cyan}${eventType}${reset}' >> ${res.status} ${res.statusText}`, - ); - }) - .catch((err) => { - console.error(`>> Failed to forward event: ${err}`); - }); - }; - - eventSource.onerror = (error) => { - if (error.code === undefined) { - return; - } - - eventSource.close(); - resume( - Effect.fail( - new OAuth.OAuthError({ - message: - error.message ?? - (error.code - ? `Event stream error (${error.code})` - : "Event stream error"), - cause: error, - }), - ), - ); - }; - }; - - connect(); - - return Effect.sync(() => { - eventSource.close(); - }); - }); - -const isUnauthorized = (error: OAuth.OAuthError) => - (typeof error.cause === "object" && - error.cause !== null && - "code" in error.cause && - (error.cause as { code?: number }).code === 401) || - error.message.includes("401"); + Effect.callback((resume) => { + let eventSource: ListenEventSource; + // Reconnections happen transparently behind the scenes; the connection + // banner should only ever be shown for the very first connection so the + // output reads as one continuous stream. + let bannerShown = false; + + const connect = () => { + eventSource = createEventSource(listenUrl, { + fetch: (input, init) => + forward(input, { + ...init, + headers: { + ...init.headers, + Authorization: `Bearer ${accessToken}`, + }, + }), + }); + + eventSource.onmessage = (event) => { + const json = JSON.parse(event.data); + const ack = Schema.decodeUnknownExit(ListenAck)(json); + + if (Exit.isSuccess(ack)) { + if (bannerShown) { + return; + } + bannerShown = true; + + const { secret } = ack.value; + const dim = "\x1b[2m"; + const bold = "\x1b[1m"; + const cyan = "\x1b[36m"; + const reset = "\x1b[0m"; + + console.log(""); + console.log( + ` ${bold}${cyan}Connected${reset} ${bold}${organizationName}${reset}`, + ); + console.log(` ${dim}Secret${reset} ${secret}`); + console.log(` ${dim}Forwarding${reset} ${forwardUrl}`); + console.log(""); + console.log(` ${dim}Waiting for events...${reset}`); + console.log(""); + + return; + } + + const reconnect = Schema.decodeUnknownExit(ListenReconnect)(json); + + if (Exit.isSuccess(reconnect)) { + // The server is about to drop the connection; tear down the current + // stream and immediately establish a new one. + eventSource.close(); + connect(); + return; + } + + const webhookEvent = Schema.decodeUnknownExit(ListenWebhookEvent)(json); + + if (Exit.isFailure(webhookEvent)) { + console.error(">> Failed to decode event"); + return; + } + + const rawPayload = webhookEvent.value.payload.payload; + + const eventType = (() => { + try { + const parsed = JSON.parse(rawPayload) as { type?: string }; + return parsed.type ?? "event"; + } catch { + return "event"; + } + })(); + + forward(forwardUrl, { + method: "POST", + headers: webhookEvent.value.headers, + body: rawPayload, + }) + .then((res) => { + const cyan = "\x1b[36m"; + const reset = "\x1b[0m"; + console.log( + `>> '${cyan}${eventType}${reset}' >> ${res.status} ${res.statusText}`, + ); + }) + .catch((err) => { + console.error(`>> Failed to forward event: ${err}`); + }); + }; + + eventSource.onerror = (error) => { + if (error.code === undefined) { + return; + } + + eventSource.close(); + resume( + Effect.fail( + new ListenError({ + code: error.code, + message: + error.message ?? + (error.code + ? `Event stream error (${error.code})` + : "Event stream error"), + cause: error, + }), + ), + ); + }; + }; + + connect(); + + return Effect.sync(() => { + eventSource.close(); + }); + }); export const listen = Command.make("listen", { url }, ({ url }) => - Effect.gen(function* () { - const environment = yield* environmentPrompt; - const oauth = yield* OAuth.OAuth; - const organization = yield* organizationLoginPrompt(environment); - const listenUrl = `${LISTEN_BASE_URLS[environment]}/${organization.id}`; - - const listenWithToken = ( - token: Token, - retried = false, - ): Effect.Effect => - startListening({ - listenUrl, - forwardUrl: url, - organizationName: organization.name, - accessToken: Redacted.value(token.token), - }).pipe( - Effect.catchTag("OAuthError", (error) => { - if (retried || !isUnauthorized(error)) { - return Effect.fail(error); - } - - return oauth - .login(environment) - .pipe( - Effect.flatMap((newToken) => listenWithToken(newToken, true)), - ); - }), - ); - - const token = yield* oauth.resolveAccessToken(environment); - yield* listenWithToken(token); - }), + Effect.gen(function* () { + const environment = yield* environmentPrompt; + const oauth = yield* OAuth.OAuth; + const organization = yield* organizationLoginPrompt(environment); + const listenUrl = `${LISTEN_BASE_URLS[environment]}/${organization.id}`; + + const listenWithToken = ( + token: Token, + retried = false, + ): Effect.Effect => + startListening({ + listenUrl, + forwardUrl: url, + organizationName: organization.name, + accessToken: Redacted.value(token.token), + }).pipe( + Effect.catchTag("ListenError", (error) => { + if (retried || error.code !== 401) { + return Effect.fail(error); + } + + return oauth + .login(environment) + .pipe( + Effect.flatMap((newToken) => listenWithToken(newToken, true)), + ); + }), + ); + + const token = yield* oauth.resolveAccessToken(environment); + yield* listenWithToken(token); + }), ); diff --git a/src/commands/login.ts b/src/commands/login.ts index a4bbd77..30a185a 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,5 +1,5 @@ -import { Command } from "@effect/cli"; import { Console, Effect } from "effect"; +import { Command } from "effect/unstable/cli"; import { environmentPrompt } from "../prompts/environment"; import * as OAuth from "../services/oauth"; diff --git a/src/commands/logout.ts b/src/commands/logout.ts index 7bc2e58..418556e 100644 --- a/src/commands/logout.ts +++ b/src/commands/logout.ts @@ -1,5 +1,5 @@ -import { Command } from "@effect/cli"; import { Console, Effect } from "effect"; +import { Command } from "effect/unstable/cli"; import * as OAuth from "../services/oauth"; export const logout = Command.make("logout", {}, () => diff --git a/src/commands/migrate.ts b/src/commands/migrate.ts index e32df1f..f0ca7c5 100644 --- a/src/commands/migrate.ts +++ b/src/commands/migrate.ts @@ -1,5 +1,5 @@ -import { Command, Options, Prompt } from "@effect/cli"; import { Effect } from "effect"; +import { Command, Flag, Prompt } from "effect/unstable/cli"; import { apiKeyPrompt, migrationPrompt, @@ -49,7 +49,7 @@ export const migrate = Command.make("migrate", {}, () => }), ); -export const LemonSqueezyAPIKey = Options.redacted("lemonSqueezyAPIKey"); +export const LemonSqueezyAPIKey = Flag.redacted("lemonSqueezyAPIKey"); const resolveProvider = ( provider: "lemonSqueezy" | "paddle" | "stripe", diff --git a/src/commands/update.test.ts b/src/commands/update.test.ts index 7aaae8b..ad7d348 100644 --- a/src/commands/update.test.ts +++ b/src/commands/update.test.ts @@ -1,154 +1,170 @@ -import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test"; -import { mkdtemp, rm, writeFile, readFile, stat } from "fs/promises"; -import { tmpdir } from "os"; -import { join } from "path"; -import { Effect } from "effect"; +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { BunFileSystem } from "@effect/platform-bun"; +import { Effect, FileSystem, PlatformError } from "effect"; import { - getArchiveExtractionCommand, - getReleaseArchiveName, - replaceBinary, + getArchiveExtractionCommand, + getReleaseArchiveName, + replaceBinary, } from "./update"; async function makeTemp() { - return mkdtemp(join(tmpdir(), "polar-test-")); + return mkdtemp(join(tmpdir(), "polar-test-")); } describe("getReleaseArchiveName", () => { - test("uses zip archives for darwin releases", () => { - expect(getReleaseArchiveName({ os: "darwin", arch: "arm64" })).toBe( - "polar-darwin-arm64.zip", - ); - expect(getReleaseArchiveName({ os: "darwin", arch: "x64" })).toBe( - "polar-darwin-x64.zip", - ); - }); - - test("uses tar.gz archives for linux releases", () => { - expect(getReleaseArchiveName({ os: "linux", arch: "x64" })).toBe( - "polar-linux-x64.tar.gz", - ); - }); + test("uses zip archives for darwin releases", () => { + expect(getReleaseArchiveName({ os: "darwin", arch: "arm64" })).toBe( + "polar-darwin-arm64.zip", + ); + expect(getReleaseArchiveName({ os: "darwin", arch: "x64" })).toBe( + "polar-darwin-x64.zip", + ); + }); + + test("uses tar.gz archives for linux releases", () => { + expect(getReleaseArchiveName({ os: "linux", arch: "x64" })).toBe( + "polar-linux-x64.tar.gz", + ); + }); }); describe("getArchiveExtractionCommand", () => { - test("uses ditto for zip archives", () => { - expect(getArchiveExtractionCommand("/tmp/polar.zip", "/tmp/out")).toEqual([ - "ditto", - "-x", - "-k", - "/tmp/polar.zip", - "/tmp/out", - ]); - }); - - test("uses tar for tar.gz archives", () => { - expect( - getArchiveExtractionCommand("/tmp/polar.tar.gz", "/tmp/out"), - ).toEqual(["tar", "-xzf", "/tmp/polar.tar.gz", "-C", "/tmp/out"]); - }); + test("uses ditto for zip archives", () => { + expect(getArchiveExtractionCommand("/tmp/polar.zip", "/tmp/out")).toEqual([ + "ditto", + "-x", + "-k", + "/tmp/polar.zip", + "/tmp/out", + ]); + }); + + test("uses tar for tar.gz archives", () => { + expect( + getArchiveExtractionCommand("/tmp/polar.tar.gz", "/tmp/out"), + ).toEqual(["tar", "-xzf", "/tmp/polar.tar.gz", "-C", "/tmp/out"]); + }); }); describe("replaceBinary", () => { - let dir: string; - let newBinaryPath: string; - let binaryPath: string; - - beforeEach(async () => { - dir = await makeTemp(); - newBinaryPath = join(dir, "polar-new"); - binaryPath = join(dir, "polar"); - await writeFile(newBinaryPath, "#!/bin/sh\necho new"); - await writeFile(binaryPath, "#!/bin/sh\necho old"); - }); - - afterEach(async () => { - await rm(dir, { recursive: true, force: true }); - }); - - test("replaces target binary with new binary content", async () => { - await Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)); - - const content = await readFile(binaryPath, "utf8"); - expect(content).toBe("#!/bin/sh\necho new"); - }); - - test("sets executable permissions on target binary", async () => { - await Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)); - - const s = await stat(binaryPath); - // check owner execute bit - expect(s.mode & 0o111).toBeGreaterThan(0); - }); - - test("leaves no temp file behind after success", async () => { - await Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)); - - // list files in dir — only the replaced binary should remain - const { readdir } = await import("fs/promises"); - const files = await readdir(dir); - const tempFiles = files.filter((f) => f.startsWith(".polar-update-")); - expect(tempFiles).toHaveLength(0); - }); - - test("throws and cleans up temp file on non-EACCES write error", async () => { - // Simulate a generic I/O error during Bun.write (not EACCES) - const bunSpy = spyOn(Bun, "write").mockImplementationOnce(() => - Promise.reject(new Error("EIO: input/output error")), - ); - - await expect( - Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)), - ).rejects.toThrow("EIO"); - - const { readdir } = await import("fs/promises"); - const files = await readdir(dir); - const tempFiles = files.filter((f) => f.startsWith(".polar-update-")); - expect(tempFiles).toHaveLength(0); - - bunSpy.mockRestore(); - }); - - test("does not throw when EACCES triggers sudo fallback", async () => { - // Simulate EACCES on rename by mocking Bun.write to throw it - const bunSpy = spyOn(Bun, "write").mockImplementationOnce(() => { - const err: any = new Error("EACCES: permission denied"); - err.code = "EACCES"; - return Promise.reject(err); - }); - - // Mock Bun.spawn so sudo mv appears to succeed - const spawnSpy = spyOn(Bun, "spawn").mockImplementationOnce(() => ({ - exited: Promise.resolve(0), - })); - - await Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)); - - // Verify sudo mv was called with the right args - expect(spawnSpy).toHaveBeenCalledWith( - ["sudo", "mv", newBinaryPath, binaryPath], - expect.objectContaining({ stdin: "inherit" }), - ); - - bunSpy.mockRestore(); - spawnSpy.mockRestore(); - }); - - test("throws when sudo mv exits non-zero", async () => { - const bunSpy = spyOn(Bun, "write").mockImplementationOnce(() => { - const err: any = new Error("EACCES: permission denied"); - err.code = "EACCES"; - return Promise.reject(err); - }); - - const spawnSpy = spyOn(Bun, "spawn").mockImplementationOnce(() => ({ - exited: Promise.resolve(1), - })); - - await expect( - Effect.runPromise(replaceBinary(newBinaryPath, binaryPath)), - ).rejects.toThrow("sudo mv failed"); - - bunSpy.mockRestore(); - spawnSpy.mockRestore(); - }); + let dir: string; + let newBinaryPath: string; + let binaryPath: string; + let writeError: PlatformError.PlatformError | undefined; + + const runReplace = () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const error = writeError; + return yield* replaceBinary(newBinaryPath, binaryPath).pipe( + Effect.provideService(FileSystem.FileSystem, { + ...fs, + writeFile: error ? () => Effect.fail(error) : fs.writeFile, + }), + ); + }).pipe(Effect.provide(BunFileSystem.layer)); + + beforeEach(async () => { + writeError = undefined; + dir = await makeTemp(); + newBinaryPath = join(dir, "polar-new"); + binaryPath = join(dir, "polar"); + await writeFile(newBinaryPath, "#!/bin/sh\necho new"); + await writeFile(binaryPath, "#!/bin/sh\necho old"); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + test("replaces target binary with new binary content", async () => { + await Effect.runPromise(runReplace()); + + const content = await readFile(binaryPath, "utf8"); + expect(content).toBe("#!/bin/sh\necho new"); + }); + + test("sets executable permissions on target binary", async () => { + await Effect.runPromise(runReplace()); + + const s = await stat(binaryPath); + // check owner execute bit + expect(s.mode & 0o111).toBeGreaterThan(0); + }); + + test("leaves no temp file behind after success", async () => { + await Effect.runPromise(runReplace()); + + // list files in dir — only the replaced binary should remain + const { readdir } = await import("node:fs/promises"); + const files = await readdir(dir); + const tempFiles = files.filter((f) => f.startsWith(".polar-update-")); + expect(tempFiles).toHaveLength(0); + }); + + test("throws and cleans up temp file on non-permission write error", async () => { + writeError = PlatformError.systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "writeFile", + description: "EIO: input/output error", + }); + + await expect(Effect.runPromise(runReplace())).rejects.toThrow("EIO"); + + const { readdir } = await import("node:fs/promises"); + const files = await readdir(dir); + const tempFiles = files.filter((f) => f.startsWith(".polar-update-")); + expect(tempFiles).toHaveLength(0); + }); + + test("does not throw when PermissionDenied triggers sudo fallback", async () => { + writeError = PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method: "writeFile", + }); + + // Mock Bun.spawn so sudo mv appears to succeed + const spawnSpy = spyOn(Bun, "spawn").mockImplementationOnce( + () => + ({ + exited: Promise.resolve(0), + }) as ReturnType, + ); + + await Effect.runPromise(runReplace()); + + // Verify sudo mv was called with the right args + expect(spawnSpy).toHaveBeenCalledWith( + ["sudo", "mv", newBinaryPath, binaryPath], + expect.objectContaining({ stdin: "inherit" }), + ); + + spawnSpy.mockRestore(); + }); + + test("throws when sudo mv exits non-zero", async () => { + writeError = PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method: "writeFile", + }); + + const spawnSpy = spyOn(Bun, "spawn").mockImplementationOnce( + () => + ({ + exited: Promise.resolve(1), + }) as ReturnType, + ); + + await expect(Effect.runPromise(runReplace())).rejects.toThrow( + "sudo mv failed", + ); + + spawnSpy.mockRestore(); + }); }); diff --git a/src/commands/update.ts b/src/commands/update.ts index 0039186..d6594ef 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,316 +1,343 @@ -import { Command } from "@effect/cli"; -import { Console, Effect, Schema } from "effect"; -import { createHash } from "crypto"; -import { chmod, mkdtemp, rename, rm, unlink } from "fs/promises"; -import { tmpdir } from "os"; -import { dirname, join } from "path"; +import { createHash } from "node:crypto"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { BunFileSystem } from "@effect/platform-bun"; +import { Console, Data, Effect, FileSystem, Schema } from "effect"; +import { Command } from "effect/unstable/cli"; import * as OAuth from "../services/oauth"; import { VERSION } from "../version"; -const fsError = (e: unknown): Error => - Object.assign( - new Error(e instanceof Error ? e.message : String(e)), - { code: (e as any)?.code }, - ); +export class UpdateError extends Data.TaggedError("UpdateError")<{ + message: string; + cause?: unknown; +}> {} export const replaceBinary = ( - newBinaryPath: string, - binaryPath: string, -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.tryPromise({ - try: () => chmod(newBinaryPath, 0o755), - catch: () => new Error("Failed to chmod new binary"), - }); - - const tempPath = join(dirname(binaryPath), `.polar-update-${Date.now()}`); - - yield* Effect.gen(function* () { - const newBinary = yield* Effect.tryPromise({ - try: () => Bun.file(newBinaryPath).arrayBuffer(), - catch: fsError, - }); - yield* Effect.tryPromise({ - try: () => Bun.write(tempPath, newBinary), - catch: fsError, - }); - yield* Effect.tryPromise({ - try: () => rename(tempPath, binaryPath), - catch: fsError, - }); - }).pipe( - Effect.tapError(() => - Effect.promise(() => unlink(tempPath).catch(() => {})), - ), - Effect.catchAll((e: Error) => - (e as any)?.code === "EACCES" - ? Effect.gen(function* () { - const proc = Bun.spawn(["sudo", "mv", newBinaryPath, binaryPath], { - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - }); - const exitCode = yield* Effect.tryPromise({ - try: () => proc.exited, - catch: () => new Error("Failed to run sudo mv"), - }); - if (exitCode !== 0) { - return yield* Effect.fail(new Error("sudo mv failed")); - } - }) - : Effect.fail(e), - ), - ); - - yield* Effect.tryPromise({ - try: () => chmod(binaryPath, 0o755), - catch: () => new Error("Failed to chmod binary"), - }); - }); + newBinaryPath: string, + binaryPath: string, +): Effect.Effect => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs + .chmod(newBinaryPath, 0o755) + .pipe( + Effect.mapError( + (cause) => + new UpdateError({ message: "Failed to chmod new binary", cause }), + ), + ); + + const tempPath = join(dirname(binaryPath), `.polar-update-${Date.now()}`); + + yield* Effect.gen(function* () { + const newBinary = yield* fs.readFile(newBinaryPath); + yield* fs.writeFile(tempPath, newBinary); + yield* fs.rename(tempPath, binaryPath); + }).pipe( + Effect.tapError(() => fs.remove(tempPath).pipe(Effect.ignore)), + Effect.catchReason("PlatformError", "PermissionDenied", () => + Effect.gen(function* () { + const proc = Bun.spawn(["sudo", "mv", newBinaryPath, binaryPath], { + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }); + const exitCode = yield* Effect.tryPromise({ + try: () => proc.exited, + catch: (cause) => + new UpdateError({ message: "Failed to run sudo mv", cause }), + }); + if (exitCode !== 0) { + return yield* new UpdateError({ message: "sudo mv failed" }); + } + }), + ), + Effect.mapError( + (cause) => new UpdateError({ message: cause.message, cause }), + ), + ); + + yield* fs + .chmod(binaryPath, 0o755) + .pipe( + Effect.mapError( + (cause) => + new UpdateError({ message: "Failed to chmod binary", cause }), + ), + ); + }); const REPO = "polarsource/cli"; const GitHubRelease = Schema.Struct({ - tag_name: Schema.String, - assets: Schema.Array( - Schema.Struct({ - name: Schema.String, - browser_download_url: Schema.String, - }), - ), + tag_name: Schema.String, + assets: Schema.Array( + Schema.Struct({ + name: Schema.String, + browser_download_url: Schema.String, + }), + ), }); function detectPlatform(): { os: string; arch: string } { - const platform = process.platform; - const arch = process.arch; - - let os: string; - switch (platform) { - case "darwin": - os = "darwin"; - break; - case "linux": - os = "linux"; - break; - default: - throw new Error(`Unsupported OS: ${platform}`); - } - - let normalizedArch: string; - switch (arch) { - case "x64": - normalizedArch = "x64"; - break; - case "arm64": - normalizedArch = "arm64"; - break; - default: - throw new Error(`Unsupported architecture: ${arch}`); - } - - if (os === "linux" && normalizedArch === "arm64") { - throw new Error("Linux arm64 is not yet supported"); - } - - return { os, arch: normalizedArch }; + const platform = process.platform; + const arch = process.arch; + + let os: string; + switch (platform) { + case "darwin": + os = "darwin"; + break; + case "linux": + os = "linux"; + break; + default: + throw new Error(`Unsupported OS: ${platform}`); + } + + let normalizedArch: string; + switch (arch) { + case "x64": + normalizedArch = "x64"; + break; + case "arm64": + normalizedArch = "arm64"; + break; + default: + throw new Error(`Unsupported architecture: ${arch}`); + } + + if (os === "linux" && normalizedArch === "arm64") { + throw new Error("Linux arm64 is not yet supported"); + } + + return { os, arch: normalizedArch }; } export function getReleaseArchiveName(platform: { - os: string; - arch: string; + os: string; + arch: string; }): string { - const baseName = `polar-${platform.os}-${platform.arch}`; - return platform.os === "darwin" ? `${baseName}.zip` : `${baseName}.tar.gz`; + const baseName = `polar-${platform.os}-${platform.arch}`; + return platform.os === "darwin" ? `${baseName}.zip` : `${baseName}.tar.gz`; } export function getArchiveExtractionCommand( - archivePath: string, - destinationDir: string, + archivePath: string, + destinationDir: string, ): string[] { - if (archivePath.endsWith(".zip")) { - return ["ditto", "-x", "-k", archivePath, destinationDir]; - } + if (archivePath.endsWith(".zip")) { + return ["ditto", "-x", "-k", archivePath, destinationDir]; + } - if (archivePath.endsWith(".tar.gz")) { - return ["tar", "-xzf", archivePath, "-C", destinationDir]; - } + if (archivePath.endsWith(".tar.gz")) { + return ["tar", "-xzf", archivePath, "-C", destinationDir]; + } - throw new Error(`Unsupported archive format: ${archivePath}`); + throw new Error(`Unsupported archive format: ${archivePath}`); } const downloadAndUpdate = ( - release: typeof GitHubRelease.Type, - latestVersion: string, + release: typeof GitHubRelease.Type, + latestVersion: string, ) => - Effect.gen(function* () { - const bold = "\x1b[1m"; - const cyan = "\x1b[36m"; - const green = "\x1b[32m"; - const dim = "\x1b[2m"; - const reset = "\x1b[0m"; - - const { os, arch } = detectPlatform(); - const platform = `${os}-${arch}`; - const archiveName = getReleaseArchiveName({ os, arch }); - - const asset = release.assets.find((a) => a.name === archiveName); - if (!asset) { - return yield* Effect.fail( - new Error(`No release asset found for platform: ${platform}`), - ); - } - - const checksumsAsset = release.assets.find( - (a) => a.name === "checksums.txt", - ); - if (!checksumsAsset) { - return yield* Effect.fail( - new Error("No checksums.txt found in release"), - ); - } - - const tempDir = yield* Effect.tryPromise({ - try: () => mkdtemp(join(tmpdir(), "polar-update-")), - catch: () => new Error("Failed to create temp directory"), - }); - - yield* Effect.ensuring( - Effect.gen(function* () { - yield* Console.log(`${dim}Downloading ${latestVersion}...${reset}`); - - const archiveBuffer = yield* Effect.tryPromise({ - try: () => - fetch(asset.browser_download_url).then((res) => { - if (!res.ok) - throw new Error( - `Download failed: ${res.status} ${res.statusText}`, - ); - return res.arrayBuffer(); - }), - catch: (e) => - new Error( - `Failed to download binary: ${e instanceof Error ? e.message : e}`, - ), - }); - - const archivePath = join(tempDir, archiveName); - yield* Effect.tryPromise({ - try: () => Bun.write(archivePath, archiveBuffer), - catch: () => new Error("Failed to write archive to disk"), - }); - - yield* Console.log(`${dim}Verifying checksum...${reset}`); - - const checksumsText = yield* Effect.tryPromise({ - try: () => - fetch(checksumsAsset.browser_download_url).then((res) => { - if (!res.ok) throw new Error("Failed to download checksums"); - return res.text(); - }), - catch: () => new Error("Failed to download checksums.txt"), - }); - - const expectedChecksum = checksumsText - .split("\n") - .find((line) => line.includes(archiveName)) - ?.split(/\s+/)[0]; - - if (!expectedChecksum) { - return yield* Effect.fail( - new Error(`No checksum found for ${archiveName}`), - ); - } - - const archiveData = yield* Effect.tryPromise({ - try: () => Bun.file(archivePath).arrayBuffer() as Promise, - catch: () => new Error("Failed to read archive for checksum"), - }); - - const hash = createHash("sha256"); - hash.update(new Uint8Array(archiveData)); - const actualChecksum = hash.digest("hex"); - - if (expectedChecksum !== actualChecksum) { - return yield* Effect.fail( - new Error( - `Checksum mismatch!\n Expected: ${expectedChecksum}\n Got: ${actualChecksum}`, - ), - ); - } - - yield* Console.log(`${dim}Extracting...${reset}`); - - const extract = Bun.spawn( - getArchiveExtractionCommand(archivePath, tempDir), - { - stdout: "ignore", - stderr: "pipe", - }, - ); - - const extractExitCode = yield* Effect.tryPromise({ - try: () => extract.exited, - catch: () => new Error("Failed to extract archive"), - }); - - if (extractExitCode !== 0) { - const stderr = yield* Effect.tryPromise({ - try: () => new Response(extract.stderr).text(), - catch: () => new Error("Failed to read archive extractor stderr"), - }); - return yield* Effect.fail( - new Error(`Failed to extract archive: ${stderr}`), - ); - } - - const binaryPath = process.execPath; - const newBinaryPath = join(tempDir, "polar"); - - yield* Console.log(`${dim}Replacing binary...${reset}`); - - yield* replaceBinary(newBinaryPath, binaryPath); - - yield* Console.log(""); - yield* Console.log( - ` ${bold}${green}Updated successfully!${reset} ${dim}${VERSION}${reset} -> ${bold}${cyan}${latestVersion}${reset}`, - ); - yield* Console.log(""); - }), - Effect.promise(() => - rm(tempDir, { recursive: true, force: true }).catch(() => {}), - ), - ); - }); + Effect.gen(function* () { + const bold = "\x1b[1m"; + const cyan = "\x1b[36m"; + const green = "\x1b[32m"; + const dim = "\x1b[2m"; + const reset = "\x1b[0m"; + + const { os, arch } = detectPlatform(); + const platform = `${os}-${arch}`; + const archiveName = getReleaseArchiveName({ os, arch }); + + const asset = release.assets.find((a) => a.name === archiveName); + if (!asset) { + return yield* new UpdateError({ + message: `No release asset found for platform: ${platform}`, + }); + } + + const checksumsAsset = release.assets.find( + (a) => a.name === "checksums.txt", + ); + if (!checksumsAsset) { + return yield* new UpdateError({ + message: "No checksums.txt found in release", + }); + } + + const tempDir = yield* Effect.tryPromise({ + try: () => mkdtemp(join(tmpdir(), "polar-update-")), + catch: (cause) => + new UpdateError({ message: "Failed to create temp directory", cause }), + }); + + yield* Effect.ensuring( + Effect.gen(function* () { + yield* Console.log(`${dim}Downloading ${latestVersion}...${reset}`); + + const archiveBuffer = yield* Effect.tryPromise({ + try: () => + fetch(asset.browser_download_url).then((res) => { + if (!res.ok) + throw new UpdateError({ + message: `Download failed: ${res.status} ${res.statusText}`, + }); + return res.arrayBuffer(); + }), + catch: (cause) => + new UpdateError({ + message: `Failed to download binary: ${cause instanceof Error ? cause.message : cause}`, + cause, + }), + }); + + const archivePath = join(tempDir, archiveName); + yield* Effect.tryPromise({ + try: () => Bun.write(archivePath, archiveBuffer), + catch: (cause) => + new UpdateError({ + message: "Failed to write archive to disk", + cause, + }), + }); + + yield* Console.log(`${dim}Verifying checksum...${reset}`); + + const checksumsText = yield* Effect.tryPromise({ + try: () => + fetch(checksumsAsset.browser_download_url).then((res) => { + if (!res.ok) + throw new UpdateError({ + message: "Failed to download checksums", + }); + return res.text(); + }), + catch: (cause) => + new UpdateError({ + message: "Failed to download checksums.txt", + cause, + }), + }); + + const expectedChecksum = checksumsText + .split("\n") + .find((line) => line.includes(archiveName)) + ?.split(/\s+/)[0]; + + if (!expectedChecksum) { + return yield* new UpdateError({ + message: `No checksum found for ${archiveName}`, + }); + } + + const archiveData = yield* Effect.tryPromise({ + try: () => + Bun.file(archivePath).arrayBuffer() as Promise, + catch: (cause) => + new UpdateError({ + message: "Failed to read archive for checksum", + cause, + }), + }); + + const hash = createHash("sha256"); + hash.update(new Uint8Array(archiveData)); + const actualChecksum = hash.digest("hex"); + + if (expectedChecksum !== actualChecksum) { + return yield* new UpdateError({ + message: `Checksum mismatch!\n Expected: ${expectedChecksum}\n Got: ${actualChecksum}`, + }); + } + + yield* Console.log(`${dim}Extracting...${reset}`); + + const extract = Bun.spawn( + getArchiveExtractionCommand(archivePath, tempDir), + { + stdout: "ignore", + stderr: "pipe", + }, + ); + + const extractExitCode = yield* Effect.tryPromise({ + try: () => extract.exited, + catch: (cause) => + new UpdateError({ message: "Failed to extract archive", cause }), + }); + + if (extractExitCode !== 0) { + const stderr = yield* Effect.tryPromise({ + try: () => new Response(extract.stderr).text(), + catch: (cause) => + new UpdateError({ + message: "Failed to read archive extractor stderr", + cause, + }), + }); + return yield* new UpdateError({ + message: `Failed to extract archive: ${stderr}`, + }); + } + + const binaryPath = process.execPath; + const newBinaryPath = join(tempDir, "polar"); + + yield* Console.log(`${dim}Replacing binary...${reset}`); + + yield* replaceBinary(newBinaryPath, binaryPath).pipe( + Effect.provide(BunFileSystem.layer), + ); + + yield* Console.log(""); + yield* Console.log( + ` ${bold}${green}Updated successfully!${reset} ${dim}${VERSION}${reset} -> ${bold}${cyan}${latestVersion}${reset}`, + ); + yield* Console.log(""); + }), + Effect.promise(() => + rm(tempDir, { recursive: true, force: true }).catch(() => {}), + ), + ); + }); export const update = Command.make("update", {}, () => - Effect.gen(function* () { - const green = "\x1b[32m"; - const dim = "\x1b[2m"; - const reset = "\x1b[0m"; - - yield* Console.log(`${dim}Checking for updates...${reset}`); - - const response = yield* Effect.tryPromise({ - try: () => - fetch( - `https://api.github.com/repos/${REPO}/releases/latest`, - ).then((res) => res.json()), - catch: () => new Error("Failed to fetch latest release from GitHub"), - }); - - const release = yield* Schema.decodeUnknown(GitHubRelease)(response); - const latestVersion = release.tag_name; - - if (latestVersion === VERSION) { - yield* Console.log( - `${green}Already up to date${reset} ${dim}(${VERSION})${reset}`, - ); - return; - } - - yield* downloadAndUpdate(release, latestVersion); - - const oauth = yield* OAuth.OAuth; - yield* oauth.logout().pipe(Effect.catchAll(() => Effect.void)); - }), + Effect.gen(function* () { + const green = "\x1b[32m"; + const dim = "\x1b[2m"; + const reset = "\x1b[0m"; + + yield* Console.log(`${dim}Checking for updates...${reset}`); + + const response = yield* Effect.tryPromise({ + try: () => + fetch(`https://api.github.com/repos/${REPO}/releases/latest`).then( + (res) => res.json(), + ), + catch: (cause) => + new UpdateError({ + message: "Failed to fetch latest release from GitHub", + cause, + }), + }); + + const release = yield* Schema.decodeUnknownEffect(GitHubRelease)(response); + const latestVersion = release.tag_name; + + if (latestVersion === VERSION) { + yield* Console.log( + `${green}Already up to date${reset} ${dim}(${VERSION})${reset}`, + ); + return; + } + + yield* downloadAndUpdate(release, latestVersion); + + const oauth = yield* OAuth.OAuth; + yield* oauth.logout().pipe(Effect.catch(() => Effect.void)); + }), ); diff --git a/src/prompts/environment.ts b/src/prompts/environment.ts index e1887b3..818d7b5 100644 --- a/src/prompts/environment.ts +++ b/src/prompts/environment.ts @@ -1,4 +1,4 @@ -import { Prompt } from "@effect/cli"; +import { Prompt } from "effect/unstable/cli"; import type { PolarEnvironment } from "../services/oauth"; export const environmentPrompt = Prompt.select({ diff --git a/src/prompts/migration.ts b/src/prompts/migration.ts index 503768b..50774dc 100644 --- a/src/prompts/migration.ts +++ b/src/prompts/migration.ts @@ -1,46 +1,46 @@ -import { Prompt } from "@effect/cli"; import { Effect } from "effect"; +import { Prompt } from "effect/unstable/cli"; import type * as LemonSqueezy from "../services/migration/lemon/provider"; const migrationProviders = [ - { value: "lemonSqueezy", title: "Lemon Squeezy" }, - { value: "paddle", title: "Paddle", disabled: true }, - { value: "stripe", title: "Stripe", disabled: true }, + { value: "lemonSqueezy", title: "Lemon Squeezy" }, + { value: "paddle", title: "Paddle", disabled: true }, + { value: "stripe", title: "Stripe", disabled: true }, ] as const; export const providerPrompt = Prompt.select({ - message: "Select Migration Provider", - choices: migrationProviders, + message: "Select Migration Provider", + choices: migrationProviders, }); export const migrationPrompt = Prompt.multiSelect({ - message: "Select Entities to Migrate", - choices: [ - { value: "products", title: "Products" }, - { value: "customers", title: "Customers" }, - ], + message: "Select Entities to Migrate", + choices: [ + { value: "products", title: "Products" }, + { value: "customers", title: "Customers" }, + ], }); export const apiKeyPrompt = Prompt.text({ - message: "Enter the API Key", - validate: (value) => { - if (value.length === 0) { - return Effect.fail("API Key is required"); - } + message: "Enter the API Key", + validate: (value) => { + if (value.length === 0) { + return Effect.fail("API Key is required"); + } - return Effect.succeed(value); - }, + return Effect.succeed(value); + }, }); export const storePrompt = (provider: LemonSqueezy.LemonSqueezyImpl) => - Effect.gen(function* () { - const stores = yield* provider.stores(); + Effect.gen(function* () { + const stores = yield* provider.stores(); - return yield* Prompt.select({ - message: "Select Store to Migrate", - choices: stores.map((store) => ({ - value: store.id, - title: store.attributes.name, - })), - }); - }); + return yield* Prompt.select({ + message: "Select Store to Migrate", + choices: stores.map((store) => ({ + value: store.id, + title: store.attributes.name, + })), + }); + }); diff --git a/src/prompts/organizations.ts b/src/prompts/organizations.ts index fd2b999..d03bed2 100644 --- a/src/prompts/organizations.ts +++ b/src/prompts/organizations.ts @@ -1,5 +1,5 @@ -import { Prompt } from "@effect/cli"; import { Effect } from "effect"; +import { Prompt } from "effect/unstable/cli"; import { OrganizationCreate } from "../schemas/Organization"; import type { PolarEnvironment } from "../services/oauth"; import * as Polar from "../services/polar"; @@ -97,7 +97,12 @@ export const organizationLoginPrompt = ( })), }); - const selected = organizations.find((org) => org.id === selectedId)!; + const selected = organizations.find((org) => org.id === selectedId); + if (!selected) { + return yield* new Polar.PolarError({ + message: "Selected organization was not found", + }); + } return { id: selected.id, slug: selected.slug, name: selected.name }; }); diff --git a/src/schemas/Customer.ts b/src/schemas/Customer.ts index 6003680..634f132 100644 --- a/src/schemas/Customer.ts +++ b/src/schemas/Customer.ts @@ -1,15 +1,16 @@ +import { AddressInputCountryAlpha2Input } from "@polar-sh/sdk/models/components/addressinput.js"; import { Schema } from "effect"; export const CustomerCreate = Schema.Struct({ - name: Schema.String, - email: Schema.String, - billingAddress: Schema.optional( - Schema.Struct({ - country: Schema.String, - city: Schema.NullOr(Schema.String), - state: Schema.NullOr(Schema.String), - }) - ), + name: Schema.String, + email: Schema.String, + billingAddress: Schema.optional( + Schema.Struct({ + country: Schema.Enum(AddressInputCountryAlpha2Input), + city: Schema.NullOr(Schema.String), + state: Schema.NullOr(Schema.String), + }), + ), }); -export type CustomerCreate = Schema.Schema.Type; +export type CustomerCreate = typeof CustomerCreate.Type; diff --git a/src/schemas/Events.ts b/src/schemas/Events.ts index 1b3520c..fbc1db8 100644 --- a/src/schemas/Events.ts +++ b/src/schemas/Events.ts @@ -1,29 +1,33 @@ import { Schema } from "effect"; export const ListenAck = Schema.Struct({ - key: Schema.Literal("connected"), - ts: Schema.DateFromString, - secret: Schema.String, + key: Schema.Literal("connected"), + ts: Schema.DateFromString, + secret: Schema.String, }); export const ListenReconnect = Schema.Struct({ - type: Schema.Literal("reconnect"), + type: Schema.Literal("reconnect"), }); export const ListenWebhookEvent = Schema.Struct({ - id: Schema.String, - key: Schema.String, - payload: Schema.Struct({ - webhook_event_id: Schema.String, - payload: Schema.String, - }), - headers: Schema.Struct({ - "user-agent": Schema.Literal("polar.sh webhooks"), - "content-type": Schema.Literal("application/json"), - "webhook-id": Schema.String, - "webhook-timestamp": Schema.String, - "webhook-signature": Schema.String, - }), + id: Schema.String, + key: Schema.String, + payload: Schema.Struct({ + webhook_event_id: Schema.String, + payload: Schema.String, + }), + headers: Schema.Struct({ + "user-agent": Schema.Literal("polar.sh webhooks"), + "content-type": Schema.Literal("application/json"), + "webhook-id": Schema.String, + "webhook-timestamp": Schema.String, + "webhook-signature": Schema.String, + }), }); -export const ListenEvent = Schema.Union(ListenAck, ListenReconnect, ListenWebhookEvent); +export const ListenEvent = Schema.Union([ + ListenAck, + ListenReconnect, + ListenWebhookEvent, +]); diff --git a/src/schemas/Migration.ts b/src/schemas/Migration.ts index 2c09a96..6934c54 100644 --- a/src/schemas/Migration.ts +++ b/src/schemas/Migration.ts @@ -1,19 +1,17 @@ import { Schema } from "effect"; export const MigrationOrigin = Schema.String.pipe( - Schema.brand("MigrationOrigin") + Schema.brand("MigrationOrigin"), ); -export type MigrationOrigin = Schema.Schema.Type; +export type MigrationOrigin = typeof MigrationOrigin.Type; export const MigrationDestination = Schema.String.pipe( - Schema.brand("MigrationDestination") + Schema.brand("MigrationDestination"), ); -export type MigrationDestination = Schema.Schema.Type< - typeof MigrationDestination ->; +export type MigrationDestination = typeof MigrationDestination.Type; export const MigrationContext = Schema.Struct({ - from: MigrationOrigin, - to: MigrationDestination, + from: MigrationOrigin, + to: MigrationDestination, }); -export type MigrationContext = Schema.Schema.Type; +export type MigrationContext = typeof MigrationContext.Type; diff --git a/src/schemas/Organization.ts b/src/schemas/Organization.ts index 57e2425..1740656 100644 --- a/src/schemas/Organization.ts +++ b/src/schemas/Organization.ts @@ -1,8 +1,8 @@ import { Schema } from "effect"; export const OrganizationCreate = Schema.Struct({ - name: Schema.String, - slug: Schema.String, + name: Schema.String, + slug: Schema.String, }); -export type OrganizationCreate = Schema.Schema.Type; +export type OrganizationCreate = typeof OrganizationCreate.Type; diff --git a/src/schemas/Product.ts b/src/schemas/Product.ts index 5c6d0ab..c5fa667 100644 --- a/src/schemas/Product.ts +++ b/src/schemas/Product.ts @@ -1,44 +1,36 @@ -import { Schema } from "effect"; +import { Schema, Struct } from "effect"; -export const CreateProductPriceCustom = Schema.mutable( - Schema.Struct({ - amountType: Schema.Literal("custom"), - priceCurrency: Schema.Literal("USD"), - minimumAmount: Schema.optional(Schema.Number), - maximumAmount: Schema.optional(Schema.Number), - presetAmount: Schema.optional(Schema.Number), - }) -); +export const CreateProductPriceCustom = Schema.Struct({ + amountType: Schema.Literal("custom"), + priceCurrency: Schema.Literal("USD").transform("usd"), + minimumAmount: Schema.optional(Schema.Number), + maximumAmount: Schema.optional(Schema.Number), + presetAmount: Schema.optional(Schema.Number), +}).mapFields(Struct.map(Schema.mutableKey)); -export const CreateProductPriceFree = Schema.mutable( - Schema.Struct({ - amountType: Schema.Literal("free"), - }) -); +export const CreateProductPriceFree = Schema.Struct({ + amountType: Schema.Literal("free"), +}).mapFields(Struct.map(Schema.mutableKey)); -export const CreateProductPriceFixed = Schema.mutable( - Schema.Struct({ - amountType: Schema.Literal("fixed"), - priceCurrency: Schema.Literal("usd"), - priceAmount: Schema.Number, - }) -); +export const CreateProductPriceFixed = Schema.Struct({ + amountType: Schema.Literal("fixed"), + priceCurrency: Schema.Literal("usd"), + priceAmount: Schema.Number, +}).mapFields(Struct.map(Schema.mutableKey)); -export const ProductCreate = Schema.mutable( - Schema.Struct({ - name: Schema.String, - description: Schema.String, - recurringInterval: Schema.NullOr(Schema.Literal("month", "year")), - prices: Schema.mutable( - Schema.Tuple( - Schema.Union( - CreateProductPriceCustom, - CreateProductPriceFree, - CreateProductPriceFixed - ) - ) - ), - }) -); +export const ProductCreate = Schema.Struct({ + name: Schema.String, + description: Schema.String, + recurringInterval: Schema.NullOr(Schema.Literals(["month", "year"])), + prices: Schema.mutable( + Schema.Tuple([ + Schema.Union([ + CreateProductPriceCustom, + CreateProductPriceFree, + CreateProductPriceFixed, + ]), + ]), + ), +}).mapFields(Struct.map(Schema.mutableKey)); -export type ProductCreate = Schema.Schema.Type; +export type ProductCreate = typeof ProductCreate.Type; diff --git a/src/schemas/Tokens.ts b/src/schemas/Tokens.ts index 903e876..9308aad 100644 --- a/src/schemas/Tokens.ts +++ b/src/schemas/Tokens.ts @@ -3,19 +3,19 @@ import { Schema } from "effect"; export const TokenScope = Schema.Array(Schema.String); export const Token = Schema.Struct({ - token: Schema.Redacted(Schema.String), - refreshToken: Schema.Redacted(Schema.String), - expiresIn: Schema.DurationFromMillis, - expiresAt: Schema.Date, - scope: TokenScope, - server: Schema.Literal("production", "sandbox"), + token: Schema.RedactedFromValue(Schema.String), + refreshToken: Schema.RedactedFromValue(Schema.String), + expiresIn: Schema.DurationFromMillis, + expiresAt: Schema.DateFromString, + scope: TokenScope, + server: Schema.Literals(["production", "sandbox"]), }); -export type Token = Schema.Schema.Type; -export type TokenJSON = Schema.Schema.Encoded; +export type Token = typeof Token.Type; +export type TokenJSON = typeof Token.Encoded; export const Tokens = Schema.Struct({ - production: Schema.optional(Token), - sandbox: Schema.optional(Token), + production: Schema.optional(Token), + sandbox: Schema.optional(Token), }); -export type Tokens = Schema.Schema.Type; -export type TokensJSON = Schema.Schema.Encoded; +export type Tokens = typeof Tokens.Type; +export type TokensJSON = typeof Tokens.Encoded; diff --git a/src/services/migration/lemon/provider.ts b/src/services/migration/lemon/provider.ts index ba7766f..09fb61b 100644 --- a/src/services/migration/lemon/provider.ts +++ b/src/services/migration/lemon/provider.ts @@ -1,13 +1,13 @@ import { - getAuthenticatedUser, - type ListStores, - lemonSqueezySetup, - listCustomers, - listDiscounts, - listFiles, - listProducts, - listStores, - listVariants, + getAuthenticatedUser, + type ListStores, + lemonSqueezySetup, + listCustomers, + listDiscounts, + listFiles, + listProducts, + listStores, + listVariants, } from "@lemonsqueezy/lemonsqueezy.js"; import { Context, Data, Effect, Layer, Schema } from "effect"; import { CustomerCreate } from "../../../schemas/Customer"; @@ -16,170 +16,170 @@ import { fetchAllPages, promiseAllInBatches } from "../../../utils"; import { parseCustomers, parseVariants } from "./transform"; export class LemonSqueezyError extends Data.TaggedError("LemonSqueezyError")<{ - message: string; - cause?: unknown; + message: string; + cause?: unknown; }> {} export interface LemonSqueezyImpl { - stores: () => Effect.Effect< - ListStores["data"], - LemonSqueezyError, - never - >; - customers: ( - storeId: string - ) => Effect.Effect; - products: ( - storeId: string - ) => Effect.Effect; + stores: () => Effect.Effect; + customers: ( + storeId: string, + ) => Effect.Effect; + products: ( + storeId: string, + ) => Effect.Effect; } -export class LemonSqueezy extends Context.Tag("LemonSqueezy")< - LemonSqueezy, - LemonSqueezyImpl ->() {} +export class LemonSqueezy extends Context.Service< + LemonSqueezy, + LemonSqueezyImpl +>()("LemonSqueezy") {} export const make = (apiKey: string) => - Effect.gen(function* () { - const client = yield* createLemonClient(apiKey); + Effect.gen(function* () { + const client = yield* createLemonClient(apiKey); - return LemonSqueezy.of({ - stores: () => - Effect.tryPromise({ - try: () => - fetchAllPages((pageNumber: number) => - client - .listStores({ - page: { number: pageNumber, size: 50 }, - }) - .then((response) => ({ - data: response.data?.data, - lastPage: response.data?.meta.page.lastPage ?? 1, - })) - ), - catch: (error) => - new LemonSqueezyError({ - message: "Failed to list Lemon Squeezy stores", - cause: error, - }), - }), - customers: (storeId: string) => - Effect.tryPromise({ - try: async () => { - const customers = await fetchAllPages((pageNumber: number) => - client - .listCustomers({ - filter: { - storeId, - }, - page: { - number: pageNumber, - size: 50, - }, - }) - .then((response) => ({ - data: response.data?.data, - lastPage: response.data?.meta.page.lastPage ?? 1, - })) - ); + return LemonSqueezy.of({ + stores: () => + Effect.tryPromise({ + try: () => + fetchAllPages((pageNumber: number) => + client + .listStores({ + page: { number: pageNumber, size: 50 }, + }) + .then((response) => ({ + data: response.data?.data, + lastPage: response.data?.meta.page.lastPage ?? 1, + })), + ), + catch: (error) => + new LemonSqueezyError({ + message: "Failed to list Lemon Squeezy stores", + cause: error, + }), + }), + customers: (storeId: string) => + Effect.tryPromise({ + try: async () => { + const customers = await fetchAllPages((pageNumber: number) => + client + .listCustomers({ + filter: { + storeId, + }, + page: { + number: pageNumber, + size: 50, + }, + }) + .then((response) => ({ + data: response.data?.data, + lastPage: response.data?.meta.page.lastPage ?? 1, + })), + ); - return customers; - }, - catch: (error) => - new LemonSqueezyError({ - message: "Failed to list Lemon Squeezy customers", - cause: error, - }), - }).pipe( - Effect.map(parseCustomers), - Effect.flatMap(Schema.decodeUnknown(Schema.Array(CustomerCreate))), - Effect.catchTag( - "ParseError", - (error) => - new LemonSqueezyError({ - message: "Failed to parse customers", - cause: error, - }) - ) - ), - products: (storeId: string) => - Effect.tryPromise({ - try: async () => { - // Fetch all products for this store - const products = await fetchAllPages((pageNumber: number) => - client - .listProducts({ - filter: { storeId }, - page: { number: pageNumber, size: 50 }, - }) - .then((response) => ({ - data: response.data?.data, - lastPage: response.data?.meta.page.lastPage ?? 1, - })) - ); + return customers; + }, + catch: (error) => + new LemonSqueezyError({ + message: "Failed to list Lemon Squeezy customers", + cause: error, + }), + }).pipe( + Effect.map(parseCustomers), + Effect.flatMap( + Schema.decodeUnknownEffect(Schema.Array(CustomerCreate)), + ), + Effect.catchTag("SchemaError", (error) => + Effect.fail( + new LemonSqueezyError({ + message: "Failed to parse customers", + cause: error, + }), + ), + ), + ), + products: (storeId: string) => + Effect.tryPromise({ + try: async () => { + // Fetch all products for this store + const products = await fetchAllPages((pageNumber: number) => + client + .listProducts({ + filter: { storeId }, + page: { number: pageNumber, size: 50 }, + }) + .then((response) => ({ + data: response.data?.data, + lastPage: response.data?.meta.page.lastPage ?? 1, + })), + ); - // Fetch variants for each product in batches to respect rate limits - const variantGroups = await promiseAllInBatches( - (product) => - fetchAllPages((pageNumber: number) => - client - .listVariants({ - filter: { productId: product.id }, - page: { number: pageNumber, size: 50 }, - }) - .then((response) => ({ - data: response.data?.data, - lastPage: response.data?.meta.page.lastPage ?? 1, - })) - ), - products, - 5 - ); + // Fetch variants for each product in batches to respect rate limits + const variantGroups = await promiseAllInBatches( + (product) => + fetchAllPages((pageNumber: number) => + client + .listVariants({ + filter: { productId: product.id }, + page: { number: pageNumber, size: 50 }, + }) + .then((response) => ({ + data: response.data?.data, + lastPage: response.data?.meta.page.lastPage ?? 1, + })), + ), + products, + 5, + ); - return variantGroups.flat(); - }, - catch: (error) => - new LemonSqueezyError({ - message: "Failed to list Lemon Squeezy products", - cause: error, - }), - }).pipe( - Effect.map(parseVariants), - Effect.flatMap(Schema.decodeUnknown(Schema.Array(ProductCreate))), - Effect.catchTag( - "ParseError", - (error) => - new LemonSqueezyError({ - message: "Failed to parse products", - cause: error, - }) - ) - ), - }); - }); + return variantGroups.flat(); + }, + catch: (error) => + new LemonSqueezyError({ + message: "Failed to list Lemon Squeezy products", + cause: error, + }), + }).pipe( + Effect.map(parseVariants), + Effect.flatMap( + Schema.decodeUnknownEffect(Schema.Array(ProductCreate)), + ), + Effect.catchTag("SchemaError", (error) => + Effect.fail( + new LemonSqueezyError({ + message: "Failed to parse products", + cause: error, + }), + ), + ), + ), + }); + }); export const layer = (apiKey: string) => - Layer.scoped(LemonSqueezy, make(apiKey)); + Layer.effect(LemonSqueezy, make(apiKey)); export const createLemonClient = (apiKey: string) => - Effect.try(() => { - lemonSqueezySetup({ - apiKey, - onError: (error) => { - throw new LemonSqueezyError({ - message: "Failed to setup Lemon Squeezy client", - cause: error, - }); - }, - }); + Effect.try(() => { + lemonSqueezySetup({ + apiKey, + onError: (error) => { + throw new LemonSqueezyError({ + message: "Failed to setup Lemon Squeezy client", + cause: error, + }); + }, + }); - return { - getAuthenticatedUser, - listStores, - listProducts, - listDiscounts, - listFiles, - listVariants, - listCustomers, - }; - }); + return { + getAuthenticatedUser, + listStores, + listProducts, + listDiscounts, + listFiles, + listVariants, + listCustomers, + }; + }); diff --git a/src/services/migration/lemon/transform.ts b/src/services/migration/lemon/transform.ts index d35d7e9..1834495 100644 --- a/src/services/migration/lemon/transform.ts +++ b/src/services/migration/lemon/transform.ts @@ -1,85 +1,71 @@ import type { - ListCustomers, - ListVariants, + ListCustomers, + ListVariants, } from "@lemonsqueezy/lemonsqueezy.js"; -import type { ProductPriceCustomCreate } from "@polar-sh/sdk/models/components/productpricecustomcreate.js"; -import type { ProductPriceFixedCreate } from "@polar-sh/sdk/models/components/productpricefixedcreate.js"; -import type { ProductPriceFreeCreate } from "@polar-sh/sdk/models/components/productpricefreecreate.js"; import type { SubscriptionRecurringInterval } from "@polar-sh/sdk/models/components/subscriptionrecurringinterval.js"; +import type { ProductCreate } from "../../../schemas/Product"; export const parseCustomers = (customers: ListCustomers["data"]) => - customers.map((customer) => ({ - name: customer.attributes.name, - email: customer.attributes.email, - billingAddress: customer.attributes.country - ? { - country: customer.attributes.country, - city: customer.attributes.city, - state: customer.attributes.region, - } - : undefined, - })) ?? []; + customers.map((customer) => ({ + name: customer.attributes.name, + email: customer.attributes.email, + billingAddress: customer.attributes.country + ? { + country: customer.attributes.country, + city: customer.attributes.city, + state: customer.attributes.region, + } + : undefined, + })) ?? []; const parseInterval = ( - interval: ListVariants["data"][number]["attributes"]["interval"] + interval: ListVariants["data"][number]["attributes"]["interval"], ): SubscriptionRecurringInterval | null => { - switch (interval) { - case "month": - return "month"; - case "year": - return "year"; - default: - return null; - } + switch (interval) { + case "month": + return "month"; + case "year": + return "year"; + default: + return null; + } }; export const parseVariants = (variants: ListVariants["data"]) => - variants.map((variant) => ({ - name: variant.attributes.name, - description: variant.attributes.description, - prices: [parsePrice(variant)], - recurringInterval: parseInterval(variant.attributes.interval), - })) ?? []; + variants.map((variant) => ({ + name: variant.attributes.name, + description: variant.attributes.description, + prices: [parsePrice(variant)], + recurringInterval: parseInterval(variant.attributes.interval), + })) ?? []; export const parsePrice = ( - variant: ListVariants["data"][number] -): - | ProductPriceFixedCreate - | ProductPriceFreeCreate - | ProductPriceCustomCreate => { - const priceCurrency = "usd"; - const priceAmount = variant.attributes.price; + variant: ListVariants["data"][number], +): (typeof ProductCreate.Encoded)["prices"][number] => { + const priceCurrency = "usd"; + const priceAmount = variant.attributes.price; - if (priceAmount > 0) { - return { - amountType: "fixed", - priceAmount, - priceCurrency, - }; - } + if (priceAmount > 0) { + return { + amountType: "fixed", + priceAmount, + priceCurrency, + }; + } - const payWhatYouWant = variant.attributes.pay_what_you_want; + const payWhatYouWant = variant.attributes.pay_what_you_want; - if (payWhatYouWant) { - return { - amountType: "custom", - priceAmount, - priceCurrency, - minimumAmount: - variant.attributes.min_price < 50 ? 50 : variant.attributes.min_price, - presetAmount: variant.attributes.suggested_price, - } as ProductPriceCustomCreate; - } + if (payWhatYouWant) { + return { + amountType: "custom", + priceCurrency: "USD", + minimumAmount: + variant.attributes.min_price < 50 ? 50 : variant.attributes.min_price, + presetAmount: variant.attributes.suggested_price, + }; + } - if (priceAmount > 0) { - return { - amountType: "fixed", - priceAmount, - priceCurrency, - } as ProductPriceFixedCreate; - } - - return { - amountType: "free", - }; + return { + amountType: "free", + }; }; diff --git a/src/services/migration/migrate.ts b/src/services/migration/migrate.ts index 26e57df..51586aa 100644 --- a/src/services/migration/migrate.ts +++ b/src/services/migration/migrate.ts @@ -1,129 +1,110 @@ import { Context, Data, Effect, Layer, Schema } from "effect"; import { CustomerCreate } from "../../schemas/Customer"; import type { MigrationContext } from "../../schemas/Migration"; -import { ProductCreate } from "../../schemas/Product"; -import * as OAuth from "../oauth"; import * as Polar from "../polar"; import type * as LemonSqueezy from "./lemon/provider"; export class MigrationError extends Data.TaggedError("MigrationError")<{ - message: string; - cause?: unknown; + message: string; + cause?: unknown; }> {} export interface MigrationImpl { - products: ( - provider: LemonSqueezy.LemonSqueezyImpl, - migration: MigrationContext - ) => Effect.Effect< - void, - LemonSqueezy.LemonSqueezyError | Polar.PolarError | OAuth.OAuthError, - Polar.Polar | OAuth.OAuth - >; - customers: ( - provider: LemonSqueezy.LemonSqueezyImpl, - migration: MigrationContext - ) => Effect.Effect< - void, - LemonSqueezy.LemonSqueezyError | Polar.PolarError | OAuth.OAuthError, - Polar.Polar | OAuth.OAuth - >; + products: ( + provider: LemonSqueezy.LemonSqueezyImpl, + migration: MigrationContext, + ) => Effect.Effect; + customers: ( + provider: LemonSqueezy.LemonSqueezyImpl, + migration: MigrationContext, + ) => Effect.Effect; } -export class Migration extends Context.Tag("Migration")< - Migration, - MigrationImpl ->() {} +export class Migration extends Context.Service()( + "Migration", +) {} export const make = Effect.gen(function* () { - return Migration.of({ - products: (provider, migration) => - Effect.gen(function* () { - const polar = yield* Polar.Polar; - // @ts-expect-error - OAuth is required for type requirements but not directly used - const _oauth = yield* OAuth.OAuth; - const providerProducts = provider.products(migration.from); + const polar = yield* Polar.Polar; - yield* providerProducts.pipe( - Effect.flatMap(Schema.encode(Schema.Array(ProductCreate))), - Effect.mapError((error) => - new Polar.PolarError({ - message: "Failed to encode products", - cause: error, - }) - ), - Effect.flatMap((products) => - Effect.all( - products.map((product) => - polar.use((client) => - Effect.tryPromise({ - try: () => - client.products.create({ - ...product, - organizationId: migration.to, - }), - catch: (error) => - new Polar.PolarError({ - message: "Failed to create product", - cause: error, - }), - }) - ) - ), - { - concurrency: 10, - } - ) - ), - Effect.tap((products) => - Effect.logDebug(`${products.length} products migrated`) - ) - ); - }), - customers: (provider, migration) => - Effect.gen(function* () { - const polar = yield* Polar.Polar; - // @ts-expect-error - OAuth is required for type requirements but not directly used - const _oauth = yield* OAuth.OAuth; - const providerCustomers = provider.customers(migration.from); + return Migration.of({ + products: (provider, migration) => + Effect.gen(function* () { + const providerProducts = provider.products(migration.from); - yield* providerCustomers.pipe( - Effect.flatMap(Schema.encode(Schema.Array(CustomerCreate))), - Effect.mapError((error) => - new Polar.PolarError({ - message: "Failed to encode customers", - cause: error, - }) - ), - Effect.flatMap((customers) => - Effect.all( - customers.map((customer) => - polar.use((client) => - Effect.tryPromise({ - try: () => - client.customers.create({ - ...customer, - organizationId: migration.to, - }), - catch: (error) => - new Polar.PolarError({ - message: "Failed to create customer", - cause: error, - }), - }) - ) - ), - { - concurrency: 10, - } - ) - ), - Effect.tap((customers) => - Effect.logDebug(`${customers.length} customers migrated`) - ) - ); - }), - }); + yield* providerProducts.pipe( + Effect.flatMap((products) => + Effect.all( + products.map((product) => + polar.use((client) => + Effect.tryPromise({ + try: () => + client.products.create({ + ...product, + organizationId: migration.to, + }), + catch: (error) => + new Polar.PolarError({ + message: "Failed to create product", + cause: error, + }), + }), + ), + ), + { + concurrency: 10, + }, + ), + ), + Effect.tap((products) => + Effect.logDebug(`${products.length} products migrated`), + ), + ); + }), + customers: (provider, migration) => + Effect.gen(function* () { + const providerCustomers = provider.customers(migration.from); + + yield* providerCustomers.pipe( + Effect.flatMap(Schema.encodeEffect(Schema.Array(CustomerCreate))), + Effect.mapError( + (error) => + new Polar.PolarError({ + message: "Failed to encode customers", + cause: error, + }), + ), + Effect.flatMap((customers) => + Effect.all( + customers.map((customer) => + polar.use((client) => + Effect.tryPromise({ + try: () => + client.customers.create({ + ...customer, + organizationId: migration.to, + }), + catch: (error) => + new Polar.PolarError({ + message: "Failed to create customer", + cause: error, + }), + }), + ), + ), + { + concurrency: 10, + }, + ), + ), + Effect.tap((customers) => + Effect.logDebug(`${customers.length} customers migrated`), + ), + ); + }), + }); }); -export const layer = Layer.scoped(Migration, make); +export const layer = Layer.effect(Migration, make).pipe( + Layer.provide(Polar.layer), +); diff --git a/src/services/oauth.ts b/src/services/oauth.ts index de4420d..d3097fa 100644 --- a/src/services/oauth.ts +++ b/src/services/oauth.ts @@ -1,12 +1,20 @@ import { createHash, randomBytes } from "node:crypto"; import { createServer, type Server } from "node:http"; +import os from "node:os"; import path from "node:path"; -import { FileSystem, Path } from "@effect/platform"; import { BunFileSystem } from "@effect/platform-bun"; import type { TokenResponse } from "@polar-sh/sdk/models/components/tokenresponse.js"; -import { Context, Data, Effect, Layer, Redacted, Schema } from "effect"; +import { + Context, + Data, + Effect, + FileSystem, + Layer, + Path, + Redacted, + Schema, +} from "effect"; import open from "open"; -import os from "os"; import { Token, Tokens } from "../schemas/Tokens"; import type { KeysToSnakeCase } from "../types"; @@ -24,68 +32,68 @@ const PRODUCTION_TOKEN_URL = "https://api.polar.sh/v1/oauth2/token"; const config = { scopes: [ - 'benefits:read', - 'benefits:write', - 'checkout_links:read', - 'checkout_links:write', - 'checkouts:read', - 'checkouts:write', - 'custom_fields:read', - 'custom_fields:write', - 'customer_meters:read', - 'customer_portal:read', - 'customer_portal:write', - 'customer_seats:read', - 'customer_seats:write', - 'customer_sessions:write', - 'customers:read', - 'customers:write', - 'discounts:read', - 'discounts:write', - 'disputes:read', - 'email', - 'events:read', - 'events:write', - 'files:read', - 'files:write', - 'license_keys:read', - 'license_keys:write', - 'member_sessions:write', - 'members:read', - 'members:write', - 'meters:read', - 'meters:write', - 'metrics:read', - 'metrics:write', - 'notification_recipients:read', - 'notification_recipients:write', - 'notifications:read', - 'notifications:write', - 'openid', - 'orders:read', - 'orders:write', - 'organization_access_tokens:read', - 'organization_access_tokens:write', - 'organizations:read', - 'organizations:write', - 'payments:read', - 'payouts:read', - 'payouts:write', - 'products:read', - 'products:write', - 'profile', - 'refunds:read', - 'refunds:write', - 'subscriptions:read', - 'subscriptions:write', - 'transactions:read', - 'transactions:write', - 'user:read', - 'user:write', - 'wallets:read', - 'wallets:write', - 'webhooks:read', - 'webhooks:write', + "benefits:read", + "benefits:write", + "checkout_links:read", + "checkout_links:write", + "checkouts:read", + "checkouts:write", + "custom_fields:read", + "custom_fields:write", + "customer_meters:read", + "customer_portal:read", + "customer_portal:write", + "customer_seats:read", + "customer_seats:write", + "customer_sessions:write", + "customers:read", + "customers:write", + "discounts:read", + "discounts:write", + "disputes:read", + "email", + "events:read", + "events:write", + "files:read", + "files:write", + "license_keys:read", + "license_keys:write", + "member_sessions:write", + "members:read", + "members:write", + "meters:read", + "meters:write", + "metrics:read", + "metrics:write", + "notification_recipients:read", + "notification_recipients:write", + "notifications:read", + "notifications:write", + "openid", + "orders:read", + "orders:write", + "organization_access_tokens:read", + "organization_access_tokens:write", + "organizations:read", + "organizations:write", + "payments:read", + "payouts:read", + "payouts:write", + "products:read", + "products:write", + "profile", + "refunds:read", + "refunds:write", + "subscriptions:read", + "subscriptions:write", + "transactions:read", + "transactions:write", + "user:read", + "user:write", + "wallets:read", + "wallets:write", + "webhooks:read", + "webhooks:write", ], redirectUrl: "http://127.0.0.1:3333/oauth/callback", }; @@ -101,7 +109,7 @@ const captureAccessTokenFromHTTPServer = (server: PolarEnvironment) => codeChallenge, ); - let httpServer: Server | null; + let httpServer: Server | null = null; // Close the HTTP server when the effect finalizes yield* Effect.addFinalizer(() => { @@ -113,7 +121,7 @@ const captureAccessTokenFromHTTPServer = (server: PolarEnvironment) => return Effect.logDebug("Temporary HTTP Server Closed"); }); - const accessToken = yield* Effect.async((resume) => { + const accessToken = yield* Effect.callback((resume) => { httpServer = createServer((request, response) => { if (httpServer !== null) { // Complete the incoming HTTP request when a login response is received @@ -142,9 +150,9 @@ const captureAccessTokenFromHTTPServer = (server: PolarEnvironment) => export class OAuthError extends Data.TaggedError("OAuthError")<{ message: string; cause?: unknown; -}> { } +}> {} -export class OAuth extends Context.Tag("OAuth")() { } +export class OAuth extends Context.Service()("OAuth") {} interface OAuthImpl { login: (server: PolarEnvironment) => Effect.Effect; @@ -169,11 +177,9 @@ export const make = Effect.gen(function* () { const token = yield* readTokenFile; if (!token || !token[server]) { - return yield* Effect.fail( - new OAuthError({ - message: "No access token found for the selected server", - }), - ); + return yield* new OAuthError({ + message: "No access token found for the selected server", + }); } return token[server]; @@ -236,7 +242,7 @@ export const make = Effect.gen(function* () { }); }); -export const layer = Layer.scoped(OAuth, make); +export const layer = Layer.effect(OAuth, make); const tokenFilePath = Effect.gen(function* () { const path = yield* Path.Path; @@ -247,20 +253,19 @@ const ensureTokenFile = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const filePath = yield* tokenFilePath; - return yield* Effect.orElse(fileSystem.access(filePath), () => + return yield* Effect.catch(fileSystem.access(filePath), () => fileSystem .makeDirectory(path.dirname(filePath), { recursive: true, }) .pipe(Effect.andThen(fileSystem.writeFileString(filePath, "{}"))), ).pipe( - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new OAuthError({ message: "Failed to ensure token file exists", cause: error, }), - ), ), ); }); @@ -281,20 +286,19 @@ const writeToTokenFile = (token: Token) => return yield* ensureTokenFile.pipe( Effect.andThen(() => - Schema.encode(Tokens)(mergedTokens).pipe( + Schema.encodeEffect(Tokens)(mergedTokens).pipe( Effect.map((encoded) => new TextEncoder().encode(JSON.stringify(encoded)), ), Effect.andThen((encoded) => fileSystem.writeFile(filePath, encoded)), ), ), - Effect.catchAll((error) => - Effect.fail( + Effect.mapError( + (error) => new OAuthError({ message: "Failed to write token to file", cause: error, }), - ), ), ); }); @@ -307,14 +311,13 @@ const readTokenFile = Effect.gen(function* () { return yield* ensureTokenFile.pipe( Effect.flatMap(() => fileSystem.readFileString(filePath)), - Effect.flatMap(Schema.decode(Schema.parseJson(Tokens))), - Effect.catchAll((error) => - Effect.fail( + Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(Tokens))), + Effect.mapError( + (error) => new OAuthError({ message: "Failed to read token file", cause: error, }), - ), ), ); }); @@ -332,9 +335,9 @@ const deleteTokenFile = Effect.gen(function* () { yield* Effect.logDebug("Deleting token file..."); - return yield* fileSystem.remove(filePath).pipe( - Effect.catchAll(() => Effect.void), - ); + return yield* fileSystem + .remove(filePath) + .pipe(Effect.catch(() => Effect.void)); }); const generateRandomString = Effect.sync(() => randomBytes(48).toString("hex")); @@ -348,8 +351,8 @@ const generateHash = (value: string) => const getClientCredentials = (server: PolarEnvironment) => server === "production" ? { - clientId: PRODUCTION_CLIENT_ID, - } + clientId: PRODUCTION_CLIENT_ID, + } : { clientId: SANDBOX_CLIENT_ID }; const buildAuthorizationUrl = ( @@ -372,7 +375,7 @@ const buildAuthorizationUrl = ( state, code_challenge: codeChallenge, code_challenge_method: "S256", - sub_type: 'user' + sub_type: "user", }); const url = `${baseUrl}?${params.toString()}`; @@ -389,11 +392,9 @@ const getLoginResult = ( const state = url.searchParams.get("state"); if (!code || !state) { - return yield* Effect.fail( - new OAuthError({ - message: "Authorization code or state is missing in the response URL", - }), - ); + return yield* new OAuthError({ + message: "Authorization code or state is missing in the response URL", + }); } return [code, state]; @@ -404,11 +405,9 @@ const refreshAccessToken = (token: Token) => const refreshToken = token.refreshToken; if (!refreshToken) { - return yield* Effect.fail( - new OAuthError({ - message: "No refresh token found", - }), - ); + return yield* new OAuthError({ + message: "No refresh token found", + }); } const { clientId } = getClientCredentials(token.server); const params = new URLSearchParams({ @@ -449,11 +448,9 @@ const refreshAccessToken = (token: Token) => }), }); - return yield* Effect.fail( - new OAuthError({ - message: `Problem encountered refreshing the access token: ${response.status}, ${details}`, - }), - ); + return yield* new OAuthError({ + message: `Problem encountered refreshing the access token: ${response.status}, ${details}`, + }); } const data = yield* Effect.tryPromise({ @@ -465,7 +462,7 @@ const refreshAccessToken = (token: Token) => }), }); - return yield* Schema.decodeUnknown(Token)({ + return yield* Schema.decodeUnknownEffect(Token)({ token: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in, @@ -473,7 +470,7 @@ const refreshAccessToken = (token: Token) => scope: data.scope.split(" "), server: token.server, }).pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail( new OAuthError({ message: "Failed to parse token response into a Token Schema", @@ -535,11 +532,9 @@ const redeemCodeForAccessToken = ( }), }); - return yield* Effect.fail( - new OAuthError({ - message: `Problem encountered redeeming the code for tokens: ${response.status}, ${details}`, - }), - ); + return yield* new OAuthError({ + message: `Problem encountered redeeming the code for tokens: ${response.status}, ${details}`, + }); } const data = yield* Effect.tryPromise({ @@ -551,7 +546,7 @@ const redeemCodeForAccessToken = ( }), }); - return yield* Schema.decodeUnknown(Token)({ + return yield* Schema.decodeUnknownEffect(Token)({ token: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in, @@ -559,7 +554,7 @@ const redeemCodeForAccessToken = ( scope: data.scope.split(" "), server, }).pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail( new OAuthError({ message: "Failed to parse token response into a Token Schema", diff --git a/src/services/polar.ts b/src/services/polar.ts index 1a6f0e6..06f2333 100644 --- a/src/services/polar.ts +++ b/src/services/polar.ts @@ -5,9 +5,9 @@ import * as OAuth from "./oauth"; export class PolarError extends Data.TaggedError("PolarError")<{ message: string; cause?: unknown; -}> { } +}> {} -export class Polar extends Context.Tag("Polar")() { } +export class Polar extends Context.Service()("Polar") {} interface PolarImpl { getClient: ( @@ -65,6 +65,6 @@ export const make = Effect.gen(function* () { }); }); -export const layer = Layer.scoped(Polar, make).pipe( +export const layer = Layer.effect(Polar, make).pipe( Layer.provide(PolarRequirementsLayer), ); diff --git a/src/services/update-check.ts b/src/services/update-check.ts index ca02bdb..910f8e8 100644 --- a/src/services/update-check.ts +++ b/src/services/update-check.ts @@ -1,7 +1,7 @@ -import { existsSync, readFileSync, mkdirSync } from "fs"; -import { writeFile } from "fs/promises"; -import { homedir } from "os"; -import { join } from "path"; +import { existsSync, mkdirSync, readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { VERSION } from "../version"; const REPO = "polarsource/cli"; diff --git a/src/types.ts b/src/types.ts index 4063a73..5b921e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,19 @@ export type CamelCase = - S extends `${infer P1}_${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase; + S extends `${infer P1}_${infer P2}${infer P3}` + ? `${Lowercase}${Uppercase}${CamelCase}` + : Lowercase; export type KeysToCamelCase = { - [K in keyof T as CamelCase]: T[K]; + [K in keyof T as CamelCase]: T[K]; }; export type CamelToSnakeCase = - S extends `${infer T}${infer U}` - ? `${T extends Capitalize - ? "_" - : ""}${Lowercase}${CamelToSnakeCase}` - : S; + S extends `${infer T}${infer U}` + ? `${T extends Capitalize + ? "_" + : ""}${Lowercase}${CamelToSnakeCase}` + : S; export type KeysToSnakeCase = { - [K in keyof T as CamelToSnakeCase]: T[K]; + [K in keyof T as CamelToSnakeCase]: T[K]; }; diff --git a/src/utils.ts b/src/utils.ts index b76194a..372a870 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,13 @@ export const slugify = (...args: string[]): string => { - const value = args.join(" "); + const value = args.join(" "); - return value - .normalize("NFD") // split an accented letter in the base letter and the acent - .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents - .toLowerCase() - .trim() - .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced) - .replace(/\s+/g, "-"); // separator + return value + .normalize("NFD") // split an accented letter in the base letter and the acent + .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents + .toLowerCase() + .trim() + .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced) + .replace(/\s+/g, "-"); // separator }; /** @@ -22,78 +22,78 @@ export const slugify = (...args: string[]): string => { * @returns {Promise} */ export async function promiseAllInBatches( - task: (item: A) => Promise, - items: A[], - batchSize: number + task: (item: A) => Promise, + items: A[], + batchSize: number, ) { - let position = 0; - let results: B[] = []; - while (position < items.length) { - const itemsForBatch = items.slice(position, position + batchSize); - results = [ - ...results, - ...(await Promise.all(itemsForBatch.map((item) => task(item)))), - ]; - position += batchSize; - } - return results; + let position = 0; + let results: B[] = []; + while (position < items.length) { + const itemsForBatch = items.slice(position, position + batchSize); + results = [ + ...results, + ...(await Promise.all(itemsForBatch.map((item) => task(item)))), + ]; + position += batchSize; + } + return results; } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const withRetry = async ( - fn: () => Promise, - maxRetries = 5, - baseDelay = 1000 + fn: () => Promise, + maxRetries = 5, + baseDelay = 1000, ): Promise => { - let lastError: unknown; - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - lastError = error; - if (attempt < maxRetries) { - const delay = baseDelay * Math.pow(2, attempt); - await sleep(delay); - } - } - } - throw lastError; + let lastError: unknown; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + if (attempt < maxRetries) { + const delay = baseDelay * 2 ** attempt; + await sleep(delay); + } + } + } + throw lastError; }; export const fetchAllPages = async ( - task: (pageNumber: number) => Promise<{ - data?: T[]; - lastPage: number; - }>, - options: { batchSize?: number; delayMs?: number } = {} + task: (pageNumber: number) => Promise<{ + data?: T[] | undefined; + lastPage: number; + }>, + options: { batchSize?: number; delayMs?: number } = {}, ): Promise => { - const { batchSize = 5, delayMs = 250 } = options; + const { batchSize = 5, delayMs = 250 } = options; - const firstPage = await withRetry(() => task(1)); - const allItems: T[] = [...(firstPage.data ?? [])]; + const firstPage = await withRetry(() => task(1)); + const allItems: T[] = [...(firstPage.data ?? [])]; - if (!firstPage.data || firstPage.lastPage <= 1) { - return allItems; - } + if (!firstPage.data || firstPage.lastPage <= 1) { + return allItems; + } - const remainingPages = Array.from( - { length: firstPage.lastPage - 1 }, - (_, i) => i + 2 - ); + const remainingPages = Array.from( + { length: firstPage.lastPage - 1 }, + (_, i) => i + 2, + ); - for (let i = 0; i < remainingPages.length; i += batchSize) { - const batch = remainingPages.slice(i, i + batchSize); - const results = await Promise.all( - batch.map((pageNumber) => withRetry(() => task(pageNumber))) - ); - for (const result of results) { - allItems.push(...(result.data ?? [])); - } - if (i + batchSize < remainingPages.length) { - await sleep(delayMs); - } - } + for (let i = 0; i < remainingPages.length; i += batchSize) { + const batch = remainingPages.slice(i, i + batchSize); + const results = await Promise.all( + batch.map((pageNumber) => withRetry(() => task(pageNumber))), + ); + for (const result of results) { + allItems.push(...(result.data ?? [])); + } + if (i + batchSize < remainingPages.length) { + await sleep(delayMs); + } + } - return allItems; + return allItems; }; diff --git a/tsconfig.json b/tsconfig.json index 28ee16f..7db8322 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,24 @@ { + "$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json", "extends": "@sindresorhus/tsconfig", "compilerOptions": { "outDir": "dist", - "skipLibCheck": true, - "module": "esnext", + "target": "ES2022", + "module": "preserve", "moduleResolution": "bundler", + "moduleDetection": "force", + "verbatimModuleSyntax": true, + "rewriteRelativeImportExtensions": true, + "noEmit": true, + "declaration": false, + "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noImplicitOverride": true, + "sourceMap": true, + "skipLibCheck": true, "plugins": [ { "name": "@effect/language-service"