diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..42710a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['18', '20', '22'] + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b3376e4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +# .github/workflows/release.yml +name: Release + +on: + push: + tags: ['v*'] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + registry-url: 'https://registry.npmjs.org' + - run: pnpm install --frozen-lockfile + - run: pnpm test + - run: pnpm build + - run: pnpm publish --provenance --no-git-checks --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5aba697 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +.DS_Store +*.log +.vitest-cache +.tsbuildinfo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..80b1696 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 theopenbee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4cf6958..9a30424 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# claude-code-cn -提供 Claude Code 中国大陆下载服务 +# @theopenbee/claude-code-cn + +Claude Code 中国大陆下载与配置工具。 + +- 默认从大陆 CDN 下载二进制(`https://dl.theopenbee.cn`) +- 交互式配置 10 个国内 Provider(KimiCode / Moonshot / DeepSeek / GLM / MiniMax / 阿里云 / 火山引擎 / 腾讯云 / 小米 Mimo / 自定义) + +## 安装 + +```bash +npm i -g @theopenbee/claude-code-cn +# 或者 +pnpm add -g @theopenbee/claude-code-cn +``` + +## 使用 + +```bash +ccc download # 下载到 ~/.claude-code-cn/bin/claude +ccc download --force # 已存在也重新下载 +ccc download --cdn-url # 覆盖 CDN + +ccc env # 交互式选择 Provider 并写入 ~/.claude/settings.json +``` + +下载完成后,请将 `~/.claude-code-cn/bin` 加入你的 `PATH`: + +```bash +export PATH="$HOME/.claude-code-cn/bin:$PATH" +``` + +## 支持平台 + +darwin-arm64 / darwin-x64 / linux-arm64 / linux-x64 / linux-arm64-musl / linux-x64-musl + +Windows 暂不支持。 + +## License + +MIT diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..476b246 --- /dev/null +++ b/biome.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "files": { "include": ["src/**/*.ts"] }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { "recommended": true } + }, + "javascript": { + "formatter": { "quoteStyle": "single", "semicolons": "always" } + }, + "organizeImports": { "enabled": true } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9e5a8f --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@theopenbee/claude-code-cn", + "version": "0.0.0", + "description": "Claude Code 中国大陆下载与配置工具", + "type": "module", + "bin": { "ccc": "./dist/cli.js" }, + "files": ["dist", "README.md", "LICENSE"], + "engines": { "node": ">=18" }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run --coverage", + "test:watch": "vitest", + "lint": "biome check .", + "format": "biome format --write .", + "typecheck": "tsc --noEmit", + "prepublishOnly": "pnpm build && pnpm test", + "release": "pnpm version patch && git push --follow-tags" + }, + "dependencies": { + "@inquirer/prompts": "^7.0.0", + "cli-progress": "^3.12.0", + "commander": "^12.1.0", + "picocolors": "^1.0.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/cli-progress": "^3.11.6", + "@types/node": "^22.0.0", + "tsup": "^8.3.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0", + "@vitest/coverage-v8": "^2.1.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "repository": { + "type": "git", + "url": "git+https://github.com/theopenbee/claude-code-cn.git" + }, + "license": "MIT", + "keywords": ["claude", "claude-code", "anthropic", "china", "cdn"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d29aefe --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2392 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@inquirer/prompts': + specifier: ^7.0.0 + version: 7.10.1(@types/node@22.19.19) + cli-progress: + specifier: ^3.12.0 + version: 3.12.0 + commander: + specifier: ^12.1.0 + version: 12.1.0 + picocolors: + specifier: ^1.0.1 + version: 1.1.1 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/cli-progress': + specifier: ^3.11.6 + version: 3.11.6 + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@vitest/coverage-v8': + specifier: ^2.1.0 + version: 2.1.9(vitest@2.1.9(@types/node@22.19.19)) + tsup: + specifier: ^8.3.0 + version: 8.5.1(postcss@8.5.14)(typescript@5.9.3) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.19) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@types/cli-progress@3.11.6': + resolution: {integrity: sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/confirm@5.1.21(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/core@10.3.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/editor@4.2.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/expand@4.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.19)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/number@3.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/password@4.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/prompts@7.10.1(@types/node@22.19.19)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.19) + '@inquirer/confirm': 5.1.21(@types/node@22.19.19) + '@inquirer/editor': 4.2.23(@types/node@22.19.19) + '@inquirer/expand': 4.0.23(@types/node@22.19.19) + '@inquirer/input': 4.3.1(@types/node@22.19.19) + '@inquirer/number': 3.0.23(@types/node@22.19.19) + '@inquirer/password': 4.0.23(@types/node@22.19.19) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.19) + '@inquirer/search': 3.2.2(@types/node@22.19.19) + '@inquirer/select': 4.4.2(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/rawlist@4.1.11(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/search@3.2.2(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/select@4.4.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/type@3.0.10(@types/node@22.19.19)': + optionalDependencies: + '@types/node': 22.19.19 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@types/cli-progress@3.11.6': + dependencies: + '@types/node': 22.19.19 + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.19.19': + dependencies: + undici-types: 6.21.0 + + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.19.19))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@22.19.19) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.19))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.19) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + acorn@8.16.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chardet@2.1.1: {} + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + + cli-width@4.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.60.3 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + has-flag@4.0.0: {} + + html-escaper@2.0.2: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.0 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minipass@7.1.3: {} + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mute-stream@2.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.12: {} + + object-assign@4.1.1: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.14): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.14 + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + safer-buffer@2.1.2: {} + + semver@7.8.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.4.5 + minimatch: 10.2.5 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.14)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.14) + resolve-from: 5.0.0 + rollup: 4.60.3 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.14 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@6.21.0: {} + + vite-node@2.1.9(@types/node@22.19.19): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.19) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.19): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.14 + rollup: 4.60.3 + optionalDependencies: + '@types/node': 22.19.19 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.19.19): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.19)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.19) + vite-node: 2.1.9(@types/node@22.19.19) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.19 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + yoctocolors-cjs@2.1.3: {} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..a9fee70 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,32 @@ +import { createRequire } from 'node:module'; +import { Command } from 'commander'; +import pc from 'picocolors'; +import { runDownload } from './commands/download.js'; +import { runEnv } from './commands/env.js'; + +const require = createRequire(import.meta.url); +const pkg = require('../package.json') as { version: string }; + +const program = new Command(); +program.name('ccc').description('Claude Code 中国大陆下载与配置工具').version(pkg.version); + +program + .command('download') + .description('下载 Claude Code 二进制到 ~/.claude-code-cn/bin/claude') + .option('--force', '已存在时也重新下载', false) + .option('--cdn-url ', '覆盖默认 CDN 地址(默认 https://dl.theopenbee.cn)') + .action(async (opts) => { + await runDownload({ force: opts.force, cdnUrl: opts.cdnUrl }); + }); + +program + .command('env') + .description('交互式选择 Provider 并写入 ~/.claude/settings.json') + .action(async () => { + await runEnv(); + }); + +program.parseAsync(process.argv).catch((err) => { + process.stderr.write(pc.red(`错误: ${(err as Error).message}\n`)); + process.exit(1); +}); diff --git a/src/commands/download.ts b/src/commands/download.ts new file mode 100644 index 0000000..aa001f7 --- /dev/null +++ b/src/commands/download.ts @@ -0,0 +1,34 @@ +import { existsSync } from 'node:fs'; +import pc from 'picocolors'; +import { install } from '../core/installer.js'; +import { detectPlatform } from '../core/platform.js'; +import { resolveCDN } from '../utils/cdn.js'; +import { binDir, claudeBinPath, stateDir } from '../utils/paths.js'; + +export interface DownloadCliOptions { + force?: boolean; + cdnUrl?: string; +} + +export async function runDownload(opts: DownloadCliOptions): Promise { + const cdn = resolveCDN(opts.cdnUrl); + const dest = claudeBinPath(); + + if (!opts.force && existsSync(dest)) { + process.stdout.write(pc.green(`已安装: ${dest}\n`)); + process.stdout.write(pc.dim('使用 --force 重新下载\n')); + return; + } + + process.stdout.write(pc.dim(`使用 CDN: ${cdn}\n`)); + const path = await install({ + cdnBase: cdn, + force: Boolean(opts.force), + platform: detectPlatform(), + stateDir: stateDir(), + }); + process.stdout.write(pc.green(`Claude 已安装到: ${path}\n`)); + process.stdout.write( + pc.dim(`请将 ${binDir()} 加入 PATH,例如:\n export PATH="${binDir()}:$PATH"\n`), + ); +} diff --git a/src/commands/env.ts b/src/commands/env.ts new file mode 100644 index 0000000..87e5d78 --- /dev/null +++ b/src/commands/env.ts @@ -0,0 +1,23 @@ +import pc from 'picocolors'; +import { configureProvider } from '../providers/configure.js'; +import { InterruptedError } from '../utils/errors.js'; +import { claudeJsonPath, claudeSettingsPath } from '../utils/paths.js'; + +export async function runEnv(): Promise { + try { + const result = await configureProvider({ + settingsPath: claudeSettingsPath(), + claudeJsonPath: claudeJsonPath(), + }); + process.stdout.write(pc.green(`已写入 ${claudeSettingsPath()}\n`)); + if (result.wroteClaudeJSON) { + process.stdout.write(pc.green(`已写入 ${claudeJsonPath()}\n`)); + } + } catch (err) { + if (err instanceof InterruptedError) { + process.stdout.write(pc.dim('已取消\n')); + return; + } + throw err; + } +} diff --git a/src/core/checksum.test.ts b/src/core/checksum.test.ts new file mode 100644 index 0000000..e871f1d --- /dev/null +++ b/src/core/checksum.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { parseChecksumFile } from './checksum.js'; + +const sample = ` +abc123 claude-1.2.3-darwin-arm64 +deadbeef claude-1.2.3-linux-x64 +cafebabe other-thing +`; + +describe('parseChecksumFile', () => { + it('returns the hash for the requested asset', () => { + expect(parseChecksumFile(sample, 'claude-1.2.3-darwin-arm64')).toBe('abc123'); + expect(parseChecksumFile(sample, 'claude-1.2.3-linux-x64')).toBe('deadbeef'); + }); + + it('throws when asset is not listed', () => { + expect(() => parseChecksumFile(sample, 'claude-9.9.9-darwin-arm64')).toThrow(/未找到资产/); + }); + + it('ignores blank and malformed lines', () => { + const messy = '\n\n \nbadline_without_two_fields\nfeed claude-x'; + expect(parseChecksumFile(messy, 'claude-x')).toBe('feed'); + }); +}); diff --git a/src/core/checksum.ts b/src/core/checksum.ts new file mode 100644 index 0000000..4d73003 --- /dev/null +++ b/src/core/checksum.ts @@ -0,0 +1,11 @@ +export function parseChecksumFile(content: string, assetName: string): string { + for (const rawLine of content.split('\n')) { + const line = rawLine.trim(); + if (!line) continue; + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + const [hash, name] = parts; + if (name === assetName) return hash as string; + } + throw new Error(`未找到资产 ${assetName}`); +} diff --git a/src/core/download.test.ts b/src/core/download.test.ts new file mode 100644 index 0000000..3d44b71 --- /dev/null +++ b/src/core/download.test.ts @@ -0,0 +1,66 @@ +// src/core/download.test.ts +import { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { type Server, createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { downloadFile } from './download.js'; + +let server: Server; +let baseURL: string; + +beforeAll(async () => { + server = createServer((req, res) => { + if (req.url === '/ok') { + res.writeHead(200, { 'content-length': '5' }); + res.end('hello'); + } else if (req.url === '/big') { + const body = Buffer.alloc(1024 * 32, 0x41); + res.writeHead(200, { 'content-length': String(body.length) }); + res.end(body); + } else { + res.writeHead(404); + res.end('nope'); + } + }); + await new Promise((r) => server.listen(0, r)); + const port = (server.address() as AddressInfo).port; + baseURL = `http://127.0.0.1:${port}`; +}); + +afterAll(() => { + server.close(); +}); + +describe('downloadFile', () => { + it('writes the body and updates the supplied hash', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dl-')); + const dst = join(dir, 'out.bin'); + const h = createHash('sha256'); + await downloadFile(`${baseURL}/ok`, dst, h, { showProgress: false }); + expect(readFileSync(dst, 'utf8')).toBe('hello'); + expect(h.digest('hex')).toBe( + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', + ); + rmSync(dir, { recursive: true, force: true }); + }); + + it('rejects on non-2xx', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dl-')); + const dst = join(dir, 'out.bin'); + await expect( + downloadFile(`${baseURL}/missing`, dst, null, { showProgress: false }), + ).rejects.toThrow(/404/); + rmSync(dir, { recursive: true, force: true }); + }); + + it('handles binary bodies of nontrivial size', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dl-')); + const dst = join(dir, 'out.bin'); + await downloadFile(`${baseURL}/big`, dst, null, { showProgress: false }); + expect(readFileSync(dst).length).toBe(1024 * 32); + rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/src/core/download.ts b/src/core/download.ts new file mode 100644 index 0000000..c769c8a --- /dev/null +++ b/src/core/download.ts @@ -0,0 +1,58 @@ +// src/core/download.ts +import type { Hash } from 'node:crypto'; +import { once } from 'node:events'; +import { createWriteStream } from 'node:fs'; +import { Presets, SingleBar } from 'cli-progress'; + +export interface DownloadOptions { + showProgress?: boolean; + label?: string; +} + +export async function downloadFile( + url: string, + destPath: string, + hash: Hash | null, + opts: DownloadOptions = {}, +): Promise { + const res = await fetch(url); + if (!res.ok || !res.body) { + throw new Error(`下载失败: ${url} (HTTP ${res.status})`); + } + + const total = Number(res.headers.get('content-length') ?? 0); + const bar = + opts.showProgress !== false && total > 0 + ? new SingleBar( + { format: `${opts.label ?? '下载中'} [{bar}] {percentage}% | {value}/{total} bytes` }, + Presets.shades_classic, + ) + : null; + bar?.start(total, 0); + + const file = createWriteStream(destPath); + const reader = (res.body as ReadableStream).getReader(); + let received = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + if (!value) continue; + if (hash) hash.update(value); + received += value.length; + bar?.update(received); + if (!file.write(Buffer.from(value))) { + await once(file, 'drain'); + } + } + } catch (err) { + file.destroy(); + throw err; + } finally { + bar?.stop(); + } + await new Promise((resolve, reject) => { + file.once('error', reject); + file.end(() => resolve()); + }); +} diff --git a/src/core/installer.test.ts b/src/core/installer.test.ts new file mode 100644 index 0000000..6dc8911 --- /dev/null +++ b/src/core/installer.test.ts @@ -0,0 +1,166 @@ +// src/core/installer.test.ts +import { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { type Server, createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { install } from './installer.js'; + +const fakeBinary = Buffer.from('FAKE_CLAUDE_BINARY_BODY'); +const fakeHash = createHash('sha256').update(fakeBinary).digest('hex'); +const platformAssetName = 'claude-1.2.3-linux-x64'; +const checksumsBody = `${fakeHash} ${platformAssetName}\n`; + +let server: Server; +let cdn: string; + +beforeAll(async () => { + server = createServer((req, res) => { + if (req.url === '/claude-code-releases/latest.txt') { + res.writeHead(200); + res.end('1.2.3'); + } else if (req.url === '/claude-code-releases/1.2.3/checksums-sha256.txt') { + res.writeHead(200); + res.end(checksumsBody); + } else if (req.url === '/claude-code-releases/1.2.3/linux-x64/claude') { + res.writeHead(200, { 'content-length': String(fakeBinary.length) }); + res.end(fakeBinary); + } else { + res.writeHead(404); + res.end('nope'); + } + }); + await new Promise((r) => server.listen(0, r)); + cdn = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; +}); + +afterAll(() => server.close()); + +describe('install', () => { + it('downloads, verifies, chmods and renames to destPath', async () => { + const stateDir = mkdtempSync(join(tmpdir(), 'inst-')); + const dest = await install({ + cdnBase: cdn, + force: false, + platform: { os: 'linux', arch: 'x64', variant: '' }, + stateDir, + showProgress: false, + }); + expect(dest).toBe(join(stateDir, 'bin', 'claude')); + expect(readFileSync(dest)).toEqual(fakeBinary); + // mode includes execute bit + expect(statSync(dest).mode & 0o111).not.toBe(0); + rmSync(stateDir, { recursive: true, force: true }); + }); + + it('skips when binary exists and force=false', async () => { + const stateDir = mkdtempSync(join(tmpdir(), 'inst-')); + const dest = join(stateDir, 'bin', 'claude'); + // pre-create + const { mkdirSync } = await import('node:fs'); + mkdirSync(join(stateDir, 'bin'), { recursive: true }); + writeFileSync(dest, 'preexisting'); + const out = await install({ + cdnBase: cdn, + force: false, + platform: { os: 'linux', arch: 'x64', variant: '' }, + stateDir, + showProgress: false, + }); + expect(out).toBe(dest); + expect(readFileSync(dest, 'utf8')).toBe('preexisting'); + rmSync(stateDir, { recursive: true, force: true }); + }); + + it('throws UnsupportedPlatformError on unsupported platform', async () => { + const stateDir = mkdtempSync(join(tmpdir(), 'inst-')); + await expect( + install({ + cdnBase: cdn, + force: false, + platform: { os: 'win32', arch: 'x64', variant: '' }, + stateDir, + showProgress: false, + }), + ).rejects.toThrow(/win32/); + rmSync(stateDir, { recursive: true, force: true }); + }); + + it('throws on SHA-256 mismatch and cleans up the tmp file', async () => { + // Set up a server whose binary body doesn't match the checksum file + const badServer = createServer((req, res) => { + if (req.url === '/claude-code-releases/latest.txt') { + res.writeHead(200); + res.end('1.2.3'); + } else if (req.url === '/claude-code-releases/1.2.3/checksums-sha256.txt') { + // Claim a hash that won't match the body below + res.writeHead(200); + res.end( + '0000000000000000000000000000000000000000000000000000000000000000 claude-1.2.3-linux-x64\n', + ); + } else if (req.url === '/claude-code-releases/1.2.3/linux-x64/claude') { + res.writeHead(200, { 'content-length': '5' }); + res.end('XXXXX'); + } else { + res.writeHead(404); + res.end(); + } + }); + await new Promise((r) => badServer.listen(0, r)); + const badCdn = `http://127.0.0.1:${(badServer.address() as AddressInfo).port}`; + const stateDir = mkdtempSync(join(tmpdir(), 'inst-')); + try { + await expect( + install({ + cdnBase: badCdn, + force: false, + platform: { os: 'linux', arch: 'x64', variant: '' }, + stateDir, + showProgress: false, + }), + ).rejects.toThrow(/SHA-256 不匹配/); + // The .tmp file should be cleaned up; destPath should not exist + expect(() => statSync(join(stateDir, 'bin', 'claude'))).toThrow(); + } finally { + badServer.close(); + rmSync(stateDir, { recursive: true, force: true }); + } + }); + + it('falls back to no-verify when checksums file is 404 and still installs', async () => { + // Server that 404s the checksum file but serves a valid binary + const partialServer = createServer((req, res) => { + if (req.url === '/claude-code-releases/latest.txt') { + res.writeHead(200); + res.end('1.2.3'); + } else if (req.url === '/claude-code-releases/1.2.3/checksums-sha256.txt') { + res.writeHead(404); + res.end('nope'); + } else if (req.url === '/claude-code-releases/1.2.3/linux-x64/claude') { + res.writeHead(200, { 'content-length': String(fakeBinary.length) }); + res.end(fakeBinary); + } else { + res.writeHead(404); + res.end(); + } + }); + await new Promise((r) => partialServer.listen(0, r)); + const partialCdn = `http://127.0.0.1:${(partialServer.address() as AddressInfo).port}`; + const stateDir = mkdtempSync(join(tmpdir(), 'inst-')); + try { + const dest = await install({ + cdnBase: partialCdn, + force: false, + platform: { os: 'linux', arch: 'x64', variant: '' }, + stateDir, + showProgress: false, + }); + expect(readFileSync(dest)).toEqual(fakeBinary); + } finally { + partialServer.close(); + rmSync(stateDir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/core/installer.ts b/src/core/installer.ts new file mode 100644 index 0000000..45b4351 --- /dev/null +++ b/src/core/installer.ts @@ -0,0 +1,94 @@ +// src/core/installer.ts +import { createHash } from 'node:crypto'; +import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import pc from 'picocolors'; +import { UnsupportedPlatformError } from '../utils/errors.js'; +import { parseChecksumFile } from './checksum.js'; +import { downloadFile } from './download.js'; +import { type Platform, buildAssetName, isSupportedPlatform, platformString } from './platform.js'; +import { fetchLatestVersion } from './version.js'; + +export interface InstallOptions { + cdnBase: string; + force: boolean; + platform: Platform; + stateDir: string; + showProgress?: boolean; +} + +export async function install(opts: InstallOptions): Promise { + const binDir = join(opts.stateDir, 'bin'); + const destPath = join(binDir, 'claude'); + + if (!opts.force) { + try { + await stat(destPath); + return destPath; + } catch { + // does not exist — continue + } + } + + if (!isSupportedPlatform(opts.platform)) { + throw new UnsupportedPlatformError(opts.platform.os, opts.platform.arch); + } + + await mkdir(binDir, { recursive: true }); + + process.stdout.write('正在获取最新版本...\n'); + const version = await fetchLatestVersion(opts.cdnBase); + const versionNum = version.replace(/^v/, ''); + process.stdout.write(`最新版本: ${version}\n`); + + const platStr = platformString(opts.platform); + const base = `${opts.cdnBase}/claude-code-releases/${versionNum}`; + const checksumURL = `${base}/checksums-sha256.txt`; + const binaryURL = `${base}/${platStr}/claude`; + const assetName = buildAssetName(opts.platform, version); + + const tmpDir = await mkdtemp(join(tmpdir(), 'claude-code-cn-')); + const checksumPath = join(tmpDir, 'checksums-sha256.txt'); + const tmpBinaryPath = `${destPath}.tmp`; + + let checksumAvailable = true; + try { + await downloadFile(checksumURL, checksumPath, null, { showProgress: false }); + } catch (err) { + checksumAvailable = false; + process.stderr.write( + pc.yellow(`warning: 无法下载 checksums-sha256.txt, 将跳过校验 (${(err as Error).message})\n`), + ); + } + + process.stdout.write(`正在下载 Claude ${version} (${platStr})...\n`); + const hash = createHash('sha256'); + try { + await downloadFile(binaryURL, tmpBinaryPath, hash, { + showProgress: opts.showProgress !== false, + label: '下载中', + }); + + if (checksumAvailable) { + process.stdout.write('正在校验 SHA-256...\n'); + const data = await readFile(checksumPath, 'utf8'); + const expected = parseChecksumFile(data, assetName); + const actual = hash.digest('hex'); + if (actual !== expected) { + throw new Error(`SHA-256 不匹配\n expected: ${expected}\n got: ${actual}`); + } + process.stdout.write('SHA-256 校验通过。\n'); + } + + await chmod(tmpBinaryPath, 0o755); + await rename(tmpBinaryPath, destPath); + } catch (err) { + await rm(tmpBinaryPath, { force: true }); + throw err; + } finally { + await rm(tmpDir, { recursive: true, force: true }); + } + + return destPath; +} diff --git a/src/core/platform.test.ts b/src/core/platform.test.ts new file mode 100644 index 0000000..7bd770c --- /dev/null +++ b/src/core/platform.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { + type Platform, + buildAssetName, + isMuslWith, + isSupportedPlatform, + mapArch, + platformString, +} from './platform.js'; + +describe('mapArch', () => { + it.each([ + ['x64', 'x64'], + ['arm64', 'arm64'], + ['ia32', 'ia32'], + ])('maps %s -> %s', (input, want) => { + expect(mapArch(input)).toBe(want); + }); +}); + +describe('isMuslWith', () => { + it('true when glob returns matches', () => { + expect(isMuslWith(() => ['/lib/ld-musl-x86_64.so.1'])).toBe(true); + }); + it('false when no match', () => { + expect(isMuslWith(() => [])).toBe(false); + }); + it('false when glob throws', () => { + expect( + isMuslWith(() => { + throw new Error('eperm'); + }), + ).toBe(false); + }); +}); + +describe('isSupportedPlatform', () => { + const supported: Platform[] = [ + { os: 'darwin', arch: 'arm64', variant: '' }, + { os: 'darwin', arch: 'x64', variant: '' }, + { os: 'linux', arch: 'arm64', variant: '' }, + { os: 'linux', arch: 'x64', variant: '' }, + { os: 'linux', arch: 'arm64', variant: 'musl' }, + { os: 'linux', arch: 'x64', variant: 'musl' }, + ]; + const unsupported: Platform[] = [ + { os: 'win32', arch: 'x64', variant: '' }, + { os: 'darwin', arch: 'ia32', variant: '' }, + { os: 'linux', arch: 'ia32', variant: '' }, + { os: 'darwin', arch: 'arm64', variant: 'musl' }, + ]; + it.each(supported)('supports %o', (p) => { + expect(isSupportedPlatform(p)).toBe(true); + }); + it.each(unsupported)('rejects %o', (p) => { + expect(isSupportedPlatform(p)).toBe(false); + }); +}); + +describe('platformString', () => { + it('os-arch when no variant', () => { + expect(platformString({ os: 'darwin', arch: 'arm64', variant: '' })).toBe('darwin-arm64'); + }); + it('os-arch-variant when variant', () => { + expect(platformString({ os: 'linux', arch: 'x64', variant: 'musl' })).toBe('linux-x64-musl'); + }); +}); + +describe('buildAssetName', () => { + it('claude--', () => { + expect(buildAssetName({ os: 'linux', arch: 'arm64', variant: 'musl' }, 'v1.2.3')).toBe( + 'claude-1.2.3-linux-arm64-musl', + ); + }); + it('strips v prefix from version', () => { + expect(buildAssetName({ os: 'darwin', arch: 'x64', variant: '' }, '1.2.3')).toBe( + 'claude-1.2.3-darwin-x64', + ); + }); +}); diff --git a/src/core/platform.ts b/src/core/platform.ts new file mode 100644 index 0000000..bc76d7e --- /dev/null +++ b/src/core/platform.ts @@ -0,0 +1,67 @@ +import { readdirSync } from 'node:fs'; + +export type SupportedOS = 'darwin' | 'linux'; +export type Arch = string; + +export interface Platform { + os: string; + arch: string; + variant: '' | 'musl'; +} + +export function mapArch(arch: string): string { + // Node 'x64' already matches the asset naming; keep this as a hook for future translations. + return arch; +} + +// Glob abstraction kept simple: caller supplies a function that returns matches +// for a pattern. The default scans /lib for ld-musl-*.so* without pulling in +// a glob library. Node 22's fs.glob is intentionally avoided for ≥18 compatibility. +export function isMuslWith(globFn: (pattern: string) => string[]): boolean { + try { + return globFn('/lib/ld-musl-*.so*').length > 0; + } catch { + return false; + } +} + +function defaultGlob(pattern: string): string[] { + // Only the specific pattern '/lib/ld-musl-*.so*' is needed. + if (pattern !== '/lib/ld-musl-*.so*') return []; + const names = readdirSync('/lib'); + const re = /^ld-musl-.*\.so/; + return names.filter((n) => re.test(n)).map((n) => `/lib/${n}`); +} + +export function isMusl(): boolean { + return isMuslWith(defaultGlob); +} + +export function detectPlatform(): Platform { + const os = process.platform; + const arch = mapArch(process.arch); + const variant: '' | 'musl' = os === 'linux' && isMusl() ? 'musl' : ''; + return { os, arch, variant }; +} + +const SUPPORTED = new Set([ + 'darwin|arm64|', + 'darwin|x64|', + 'linux|arm64|', + 'linux|x64|', + 'linux|arm64|musl', + 'linux|x64|musl', +]); + +export function isSupportedPlatform(p: Platform): boolean { + return SUPPORTED.has(`${p.os}|${p.arch}|${p.variant}`); +} + +export function platformString(p: Platform): string { + return p.variant ? `${p.os}-${p.arch}-${p.variant}` : `${p.os}-${p.arch}`; +} + +export function buildAssetName(p: Platform, version: string): string { + const ver = version.replace(/^v/, ''); + return `claude-${ver}-${platformString(p)}`; +} diff --git a/src/core/version.test.ts b/src/core/version.test.ts new file mode 100644 index 0000000..500a65e --- /dev/null +++ b/src/core/version.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { fetchLatestVersion, normalizeTag } from './version.js'; + +describe('normalizeTag', () => { + it('adds v prefix', () => { + expect(normalizeTag('1.2.3')).toBe('v1.2.3'); + }); + it('keeps existing v', () => { + expect(normalizeTag('v1.2.3')).toBe('v1.2.3'); + }); + it('trims whitespace', () => { + expect(normalizeTag(' 1.2.3\n')).toBe('v1.2.3'); + }); + it('throws on empty', () => { + expect(() => normalizeTag('')).toThrow(/版本号为空/); + expect(() => normalizeTag(' ')).toThrow(/版本号为空/); + }); +}); + +describe('fetchLatestVersion', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('fetches /claude-code-releases/latest.txt and normalizes', async () => { + const fetchMock = vi.fn(async () => ({ + ok: true, + status: 200, + text: async () => '1.2.3\n', + })); + vi.stubGlobal('fetch', fetchMock); + const v = await fetchLatestVersion('https://cdn.test'); + expect(v).toBe('v1.2.3'); + expect(fetchMock).toHaveBeenCalledWith('https://cdn.test/claude-code-releases/latest.txt'); + }); + + it('throws on non-200', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ ok: false, status: 404, text: async () => '' })), + ); + await expect(fetchLatestVersion('https://cdn.test')).rejects.toThrow(/404/); + }); +}); diff --git a/src/core/version.ts b/src/core/version.ts new file mode 100644 index 0000000..deaf76a --- /dev/null +++ b/src/core/version.ts @@ -0,0 +1,14 @@ +export function normalizeTag(tag: string): string { + const t = tag.trim(); + if (!t) throw new Error('版本号为空'); + return t.startsWith('v') ? t : `v${t}`; +} + +export async function fetchLatestVersion(cdnBase: string): Promise { + const url = `${cdnBase}/claude-code-releases/latest.txt`; + const res = await fetch(url); + if (!res.ok) { + throw new Error(`获取最新版本失败: ${url} 返回 ${res.status}`); + } + return normalizeTag(await res.text()); +} diff --git a/src/providers/builders.test.ts b/src/providers/builders.test.ts new file mode 100644 index 0000000..d8a7845 --- /dev/null +++ b/src/providers/builders.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; +import { + aliyunEnv, + customEnv, + deepseekEnv, + glmEnv, + kimiCodeEnv, + mimoEnv, + minimaxEnv, + moonshotEnv, + tencentEnv, + volcengineEnv, +} from './builders.js'; + +describe('provider env builders', () => { + it('kimiCodeEnv', () => { + expect(kimiCodeEnv('K')).toEqual({ + ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/', + ANTHROPIC_API_KEY: 'K', + ENABLE_TOOL_SEARCH: 'false', + }); + }); + + it('moonshotEnv', () => { + expect(moonshotEnv('K')).toEqual({ + ANTHROPIC_BASE_URL: 'https://api.moonshot.cn/anthropic', + ANTHROPIC_AUTH_TOKEN: 'K', + ANTHROPIC_MODEL: 'kimi-k2.5', + ANTHROPIC_SMALL_FAST_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'kimi-k2.5', + CLAUDE_CODE_SUBAGENT_MODEL: 'kimi-k2.5', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + ENABLE_TOOL_SEARCH: 'false', + API_TIMEOUT_MS: '600000', + }); + }); + + it('deepseekEnv', () => { + expect(deepseekEnv('K')).toEqual({ + ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic', + ANTHROPIC_AUTH_TOKEN: 'K', + ANTHROPIC_MODEL: 'deepseek-chat', + ANTHROPIC_SMALL_FAST_MODEL: 'deepseek-chat', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + API_TIMEOUT_MS: '600000', + }); + }); + + it('glmEnv', () => { + expect(glmEnv('K')).toEqual({ + ANTHROPIC_AUTH_TOKEN: 'K', + ANTHROPIC_BASE_URL: 'https://open.bigmodel.cn/api/anthropic', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.5-air', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-5-turbo', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-5.1', + API_TIMEOUT_MS: '3000000', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + }); + }); + + it('minimaxEnv', () => { + expect(minimaxEnv('K')).toEqual({ + ANTHROPIC_BASE_URL: 'https://api.minimaxi.com/anthropic', + ANTHROPIC_AUTH_TOKEN: 'K', + API_TIMEOUT_MS: '3000000', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + ANTHROPIC_MODEL: 'MiniMax-M2.7', + ANTHROPIC_SMALL_FAST_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2.7', + }); + }); + + it('aliyunEnv with selected model', () => { + expect(aliyunEnv('K', 'qwen3.5-plus')).toEqual({ + ANTHROPIC_AUTH_TOKEN: 'K', + ANTHROPIC_BASE_URL: 'https://coding.dashscope.aliyuncs.com/apps/anthropic', + ANTHROPIC_MODEL: 'qwen3.5-plus', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + }); + }); + + it('volcengineEnv with selected model', () => { + expect(volcengineEnv('K', 'doubao-seed-2.0-code')).toMatchObject({ + ANTHROPIC_BASE_URL: 'https://ark.cn-beijing.volces.com/api/coding', + ANTHROPIC_MODEL: 'doubao-seed-2.0-code', + }); + }); + + it('tencentEnv with selected model', () => { + expect(tencentEnv('K', 'tc-code-latest(auto)')).toMatchObject({ + ANTHROPIC_BASE_URL: 'https://api.lkeap.cloud.tencent.com/coding/anthropic', + ANTHROPIC_MODEL: 'tc-code-latest(auto)', + }); + }); + + it('mimoEnv with user-provided baseURL', () => { + expect(mimoEnv('K', 'https://mimo.example')).toEqual({ + ANTHROPIC_BASE_URL: 'https://mimo.example', + ANTHROPIC_AUTH_TOKEN: 'K', + ANTHROPIC_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'mimo-v2.5-pro', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + API_TIMEOUT_MS: '3000000', + }); + }); + + it('customEnv just wires baseURL + token', () => { + expect(customEnv('K', 'https://custom.example')).toEqual({ + ANTHROPIC_BASE_URL: 'https://custom.example', + ANTHROPIC_AUTH_TOKEN: 'K', + }); + }); +}); diff --git a/src/providers/builders.ts b/src/providers/builders.ts new file mode 100644 index 0000000..1fb2f07 --- /dev/null +++ b/src/providers/builders.ts @@ -0,0 +1,103 @@ +import type { ProviderEnv } from './env-keys.js'; + +export function kimiCodeEnv(apiKey: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/', + ANTHROPIC_API_KEY: apiKey, + ENABLE_TOOL_SEARCH: 'false', + }; +} + +export function moonshotEnv(apiKey: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: 'https://api.moonshot.cn/anthropic', + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_MODEL: 'kimi-k2.5', + ANTHROPIC_SMALL_FAST_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'kimi-k2.5', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'kimi-k2.5', + CLAUDE_CODE_SUBAGENT_MODEL: 'kimi-k2.5', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + ENABLE_TOOL_SEARCH: 'false', + API_TIMEOUT_MS: '600000', + }; +} + +export function deepseekEnv(apiKey: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic', + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_MODEL: 'deepseek-chat', + ANTHROPIC_SMALL_FAST_MODEL: 'deepseek-chat', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + API_TIMEOUT_MS: '600000', + }; +} + +export function glmEnv(apiKey: string): ProviderEnv { + return { + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_BASE_URL: 'https://open.bigmodel.cn/api/anthropic', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.5-air', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-5-turbo', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-5.1', + API_TIMEOUT_MS: '3000000', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + }; +} + +export function minimaxEnv(apiKey: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: 'https://api.minimaxi.com/anthropic', + ANTHROPIC_AUTH_TOKEN: apiKey, + API_TIMEOUT_MS: '3000000', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + ANTHROPIC_MODEL: 'MiniMax-M2.7', + ANTHROPIC_SMALL_FAST_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2.7', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2.7', + }; +} + +function standardEnv(baseURL: string, apiKey: string, model: string): ProviderEnv { + return { + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_BASE_URL: baseURL, + ANTHROPIC_MODEL: model, + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + }; +} + +export function aliyunEnv(apiKey: string, model: string): ProviderEnv { + return standardEnv('https://coding.dashscope.aliyuncs.com/apps/anthropic', apiKey, model); +} + +export function volcengineEnv(apiKey: string, model: string): ProviderEnv { + return standardEnv('https://ark.cn-beijing.volces.com/api/coding', apiKey, model); +} + +export function tencentEnv(apiKey: string, model: string): ProviderEnv { + return standardEnv('https://api.lkeap.cloud.tencent.com/coding/anthropic', apiKey, model); +} + +export function mimoEnv(apiKey: string, baseURL: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: baseURL, + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'mimo-v2.5-pro', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'mimo-v2.5-pro', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + API_TIMEOUT_MS: '3000000', + }; +} + +export function customEnv(apiKey: string, baseURL: string): ProviderEnv { + return { + ANTHROPIC_BASE_URL: baseURL, + ANTHROPIC_AUTH_TOKEN: apiKey, + }; +} diff --git a/src/providers/configure.test.ts b/src/providers/configure.test.ts new file mode 100644 index 0000000..4175935 --- /dev/null +++ b/src/providers/configure.test.ts @@ -0,0 +1,122 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), + confirm: vi.fn(), +})); + +import { confirm, input, select } from '@inquirer/prompts'; +import { configureProvider } from './configure.js'; + +const mockedSelect = vi.mocked(select); +const mockedInput = vi.mocked(input); +const mockedConfirm = vi.mocked(confirm); + +let dir: string; +let settingsPath: string; +let claudeJsonPath: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'cfg-')); + settingsPath = join(dir, 'settings.json'); + claudeJsonPath = join(dir, 'claude.json'); + vi.clearAllMocks(); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('configureProvider', () => { + it('writes settings.json with KimiCode env when chosen', async () => { + mockedSelect.mockResolvedValueOnce('KimiCode'); // provider + mockedInput.mockResolvedValueOnce('KIMI_KEY'); // api key + + const result = await configureProvider({ settingsPath, claudeJsonPath }); + + const out = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(out.env).toMatchObject({ + ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/', + ANTHROPIC_API_KEY: 'KIMI_KEY', + }); + expect(result.wroteClaudeJSON).toBe(false); + }); + + it('prompts for baseURL before key for Custom and writes both', async () => { + mockedSelect.mockResolvedValueOnce('Custom provider'); + mockedInput + .mockResolvedValueOnce('https://x.example') // baseURL prompted first + .mockResolvedValueOnce('TOKEN'); // then key + + await configureProvider({ settingsPath, claudeJsonPath }); + + const out = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(out.env.ANTHROPIC_BASE_URL).toBe('https://x.example'); + expect(out.env.ANTHROPIC_AUTH_TOKEN).toBe('TOKEN'); + }); + + it('writes claude.json when provider has needClaudeJSON (GLM)', async () => { + mockedSelect.mockResolvedValueOnce('Zhipu (GLM)'); + mockedInput.mockResolvedValueOnce('GLM_KEY'); + + const result = await configureProvider({ settingsPath, claudeJsonPath }); + + const settings = JSON.parse(readFileSync(settingsPath, 'utf8')); + const cjson = JSON.parse(readFileSync(claudeJsonPath, 'utf8')); + expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://open.bigmodel.cn/api/anthropic'); + expect(cjson.hasCompletedOnboarding).toBe(true); + expect(result.wroteClaudeJSON).toBe(true); + }); + + it('prompts for model selection for Aliyun and writes the chosen model', async () => { + mockedSelect + .mockResolvedValueOnce('Alibaba Cloud (Qwen)') // provider + .mockResolvedValueOnce('kimi-k2.5'); // model + mockedInput.mockResolvedValueOnce('ALI_KEY'); + + await configureProvider({ settingsPath, claudeJsonPath }); + + const out = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(out.env.ANTHROPIC_BASE_URL).toBe('https://coding.dashscope.aliyuncs.com/apps/anthropic'); + expect(out.env.ANTHROPIC_AUTH_TOKEN).toBe('ALI_KEY'); + expect(out.env.ANTHROPIC_MODEL).toBe('kimi-k2.5'); + }); + + it('removes stale provider env keys before writing', async () => { + writeFileSync( + settingsPath, + JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: 'old-url', + ANTHROPIC_API_KEY: 'old-key', + UNRELATED: 'keep-me', + }, + }), + ); + // existing file → confirm("skip?") — answer no + mockedConfirm.mockResolvedValueOnce(false); + mockedSelect.mockResolvedValueOnce('DeepSeek'); + mockedInput.mockResolvedValueOnce('DS_KEY'); + + await configureProvider({ settingsPath, claudeJsonPath }); + const out = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(out.env.UNRELATED).toBe('keep-me'); + expect(out.env.ANTHROPIC_BASE_URL).toBe('https://api.deepseek.com/anthropic'); + expect(out.env.ANTHROPIC_AUTH_TOKEN).toBe('DS_KEY'); + expect(out.env.ANTHROPIC_API_KEY).toBeUndefined(); + }); + + it('skips when user confirms skip', async () => { + writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_API_KEY: 'keep' } })); + mockedConfirm.mockResolvedValueOnce(true); + + await configureProvider({ settingsPath, claudeJsonPath }); + const out = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(out.env.ANTHROPIC_API_KEY).toBe('keep'); + expect(mockedSelect).not.toHaveBeenCalled(); + }); +}); diff --git a/src/providers/configure.ts b/src/providers/configure.ts new file mode 100644 index 0000000..c185d9b --- /dev/null +++ b/src/providers/configure.ts @@ -0,0 +1,82 @@ +import { existsSync } from 'node:fs'; +import { confirm, input, select } from '@inquirer/prompts'; +import { InterruptedError } from '../utils/errors.js'; +import { mergeJSONFile } from '../utils/json-merge.js'; +import { PROVIDER_ENV_KEYS, type ProviderEnv } from './env-keys.js'; +import { PROVIDER_SPECS, type ProviderSpec } from './specs.js'; + +export interface ConfigureOptions { + settingsPath: string; + claudeJsonPath: string; +} + +function isInterrupt(err: unknown): boolean { + // @inquirer/prompts throws ExitPromptError on Ctrl+C + return (err as { name?: string } | null)?.name === 'ExitPromptError'; +} + +async function ask(fn: () => Promise): Promise { + try { + return await fn(); + } catch (err) { + if (isInterrupt(err)) throw new InterruptedError(); + throw err; + } +} + +export async function configureProvider( + opts: ConfigureOptions, +): Promise<{ wroteClaudeJSON: boolean }> { + if (existsSync(opts.settingsPath)) { + const skip = await ask(() => + confirm({ message: '已检测到现有 ~/.claude/settings.json,是否跳过?', default: true }), + ); + if (skip) return { wroteClaudeJSON: false }; + } + + const providerName = await ask(() => + select({ + message: '请选择 Provider', + choices: PROVIDER_SPECS.map((s) => ({ name: s.name, value: s.name })), + }), + ); + const spec = PROVIDER_SPECS.find((s) => s.name === providerName) as ProviderSpec; + + let baseURL = ''; + if (spec.baseURLPrompt) { + baseURL = await ask(() => input({ message: spec.baseURLPrompt as string })); + } + + const apiKey = await ask(() => input({ message: spec.keyPrompt })); + + let secondArg = ''; + if (spec.modelOptions && spec.modelOptions.length > 0) { + secondArg = await ask(() => + select({ + message: '请选择模型', + choices: (spec.modelOptions as string[]).map((m) => ({ name: m, value: m })), + default: spec.modelDefault, + }), + ); + } else if (baseURL) { + secondArg = baseURL; + } + + const env: ProviderEnv = spec.buildEnv(apiKey, secondArg); + + await mergeJSONFile(opts.settingsPath, (m) => { + const current = (m.env as Record | undefined) ?? {}; + for (const k of PROVIDER_ENV_KEYS) delete current[k]; + for (const [k, v] of Object.entries(env)) current[k] = v; + m.env = current; + }); + + if (spec.needClaudeJSON) { + await mergeJSONFile(opts.claudeJsonPath, (m) => { + m.hasCompletedOnboarding = true; + }); + return { wroteClaudeJSON: true }; + } + + return { wroteClaudeJSON: false }; +} diff --git a/src/providers/env-keys.test.ts b/src/providers/env-keys.test.ts new file mode 100644 index 0000000..53aaedf --- /dev/null +++ b/src/providers/env-keys.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { PROVIDER_ENV_KEYS } from './env-keys.js'; + +describe('PROVIDER_ENV_KEYS', () => { + it('contains the 12 anthropic-related keys', () => { + expect(PROVIDER_ENV_KEYS).toEqual([ + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_API_KEY', + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_MODEL', + 'ANTHROPIC_SMALL_FAST_MODEL', + 'ANTHROPIC_DEFAULT_SONNET_MODEL', + 'ANTHROPIC_DEFAULT_OPUS_MODEL', + 'ANTHROPIC_DEFAULT_HAIKU_MODEL', + 'CLAUDE_CODE_SUBAGENT_MODEL', + 'ENABLE_TOOL_SEARCH', + 'API_TIMEOUT_MS', + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', + ]); + }); +}); diff --git a/src/providers/env-keys.ts b/src/providers/env-keys.ts new file mode 100644 index 0000000..6bbb06c --- /dev/null +++ b/src/providers/env-keys.ts @@ -0,0 +1,16 @@ +export const PROVIDER_ENV_KEYS = [ + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_API_KEY', + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_MODEL', + 'ANTHROPIC_SMALL_FAST_MODEL', + 'ANTHROPIC_DEFAULT_SONNET_MODEL', + 'ANTHROPIC_DEFAULT_OPUS_MODEL', + 'ANTHROPIC_DEFAULT_HAIKU_MODEL', + 'CLAUDE_CODE_SUBAGENT_MODEL', + 'ENABLE_TOOL_SEARCH', + 'API_TIMEOUT_MS', + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', +] as const; + +export type ProviderEnv = Record; diff --git a/src/providers/specs.test.ts b/src/providers/specs.test.ts new file mode 100644 index 0000000..62ce8d9 --- /dev/null +++ b/src/providers/specs.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { PROVIDER_SPECS, type ProviderSpec } from './specs.js'; + +describe('PROVIDER_SPECS', () => { + it('lists 10 providers in the documented order', () => { + expect(PROVIDER_SPECS.map((s) => s.name)).toEqual([ + 'KimiCode', + 'Moonshot (Kimi)', + 'DeepSeek', + 'Zhipu (GLM)', + 'MiniMax', + 'Alibaba Cloud (Qwen)', + 'Volcengine (Doubao)', + 'Tencent Cloud', + 'Xiaomi Mimo', + 'Custom provider', + ]); + }); + + it('marks NeedClaudeJSON correctly', () => { + const map: Record = Object.fromEntries( + PROVIDER_SPECS.map((s) => [s.name, s.needClaudeJSON]), + ); + expect(map['Zhipu (GLM)']).toBe(true); + expect(map.MiniMax).toBe(true); + expect(map['Volcengine (Doubao)']).toBe(true); + expect(map['Tencent Cloud']).toBe(true); + expect(map['Xiaomi Mimo']).toBe(true); + expect(map.KimiCode).toBe(false); + expect(map['Custom provider']).toBe(false); + }); + + it('Aliyun has model options with qwen3.5-plus default', () => { + const s = PROVIDER_SPECS.find((x) => x.name === 'Alibaba Cloud (Qwen)') as ProviderSpec; + expect(s.modelOptions).toEqual(['qwen3.5-plus', 'kimi-k2.5', 'glm-5', 'MiniMax-M2.5']); + expect(s.modelDefault).toBe('qwen3.5-plus'); + }); + + it('Mimo and Custom prompt for baseURL', () => { + const mimo = PROVIDER_SPECS.find((x) => x.name === 'Xiaomi Mimo') as ProviderSpec; + const custom = PROVIDER_SPECS.find((x) => x.name === 'Custom provider') as ProviderSpec; + expect(mimo.baseURLPrompt).toBeTruthy(); + expect(custom.baseURLPrompt).toBeTruthy(); + }); + + it('builds env via buildEnv', () => { + const km = PROVIDER_SPECS.find((x) => x.name === 'KimiCode') as ProviderSpec; + expect(km.buildEnv('K', '')).toMatchObject({ ANTHROPIC_API_KEY: 'K' }); + }); +}); diff --git a/src/providers/specs.ts b/src/providers/specs.ts new file mode 100644 index 0000000..5163cb8 --- /dev/null +++ b/src/providers/specs.ts @@ -0,0 +1,112 @@ +import { + aliyunEnv, + customEnv, + deepseekEnv, + glmEnv, + kimiCodeEnv, + mimoEnv, + minimaxEnv, + moonshotEnv, + tencentEnv, + volcengineEnv, +} from './builders.js'; +import type { ProviderEnv } from './env-keys.js'; + +export interface ProviderSpec { + name: string; + keyPrompt: string; + baseURLPrompt?: string; + modelOptions?: string[]; + modelDefault?: string; + needClaudeJSON: boolean; + buildEnv: (apiKey: string, modelOrBaseURL: string) => ProviderEnv; +} + +export const PROVIDER_SPECS: readonly ProviderSpec[] = [ + { + name: 'KimiCode', + keyPrompt: '请输入 KimiCode API Key', + needClaudeJSON: false, + buildEnv: (k) => kimiCodeEnv(k), + }, + { + name: 'Moonshot (Kimi)', + keyPrompt: '请输入 Moonshot API Key', + needClaudeJSON: false, + buildEnv: (k) => moonshotEnv(k), + }, + { + name: 'DeepSeek', + keyPrompt: '请输入 DeepSeek API Key', + needClaudeJSON: false, + buildEnv: (k) => deepseekEnv(k), + }, + { + name: 'Zhipu (GLM)', + keyPrompt: '请输入 智谱 GLM API Key', + needClaudeJSON: true, + buildEnv: (k) => glmEnv(k), + }, + { + name: 'MiniMax', + keyPrompt: '请输入 MiniMax API Key', + needClaudeJSON: true, + buildEnv: (k) => minimaxEnv(k), + }, + { + name: 'Alibaba Cloud (Qwen)', + keyPrompt: '请输入 阿里云百炼 API Key', + modelOptions: ['qwen3.5-plus', 'kimi-k2.5', 'glm-5', 'MiniMax-M2.5'], + modelDefault: 'qwen3.5-plus', + needClaudeJSON: false, + buildEnv: aliyunEnv, + }, + { + name: 'Volcengine (Doubao)', + keyPrompt: '请输入 火山引擎 API Key', + modelOptions: [ + 'doubao-seed-2.0-code', + 'doubao-seed-2.0-pro', + 'doubao-seed-2.0-lite', + 'doubao-seed-code', + 'minimax-m2.5', + 'glm-4.7', + 'deepseek-v3.2', + 'kimi-k2.5', + ], + modelDefault: 'doubao-seed-2.0-code', + needClaudeJSON: true, + buildEnv: volcengineEnv, + }, + { + name: 'Tencent Cloud', + keyPrompt: '请输入 腾讯云 API Key', + modelOptions: [ + 'tc-code-latest(auto)', + 'hunyuan-2.0-instruct', + 'hunyuan-2.0-thinking', + 'minimax-m2.5', + 'kimi-k2.5', + 'glm-5', + 'hunyuan-t1', + 'hunyuan-turbos', + ], + modelDefault: 'tc-code-latest(auto)', + needClaudeJSON: true, + buildEnv: tencentEnv, + }, + { + name: 'Xiaomi Mimo', + keyPrompt: '请输入 小米 Mimo Token', + baseURLPrompt: '请输入 小米 Mimo Base URL', + needClaudeJSON: true, + buildEnv: (k, baseURL) => mimoEnv(k, baseURL), + }, + { + name: 'Custom provider', + keyPrompt: '请输入 自定义 Provider Token', + baseURLPrompt: '请输入 自定义 Provider Base URL', + needClaudeJSON: false, + buildEnv: (k, baseURL) => customEnv(k, baseURL), + }, +]; diff --git a/src/utils/cdn.test.ts b/src/utils/cdn.test.ts new file mode 100644 index 0000000..604c737 --- /dev/null +++ b/src/utils/cdn.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_CDN_BASE, resolveCDN } from './cdn.js'; + +describe('resolveCDN', () => { + it('returns user-provided URL when set', () => { + expect(resolveCDN('https://mirror.example.com')).toBe('https://mirror.example.com'); + }); + + it('falls back to default when empty', () => { + expect(resolveCDN(undefined)).toBe(DEFAULT_CDN_BASE); + expect(resolveCDN('')).toBe(DEFAULT_CDN_BASE); + }); + + it('strips trailing slash', () => { + expect(resolveCDN('https://x.test/')).toBe('https://x.test'); + }); + + it('DEFAULT_CDN_BASE is https://dl.theopenbee.cn', () => { + expect(DEFAULT_CDN_BASE).toBe('https://dl.theopenbee.cn'); + }); +}); diff --git a/src/utils/cdn.ts b/src/utils/cdn.ts new file mode 100644 index 0000000..82d15e8 --- /dev/null +++ b/src/utils/cdn.ts @@ -0,0 +1,7 @@ +export const DEFAULT_CDN_BASE = 'https://dl.theopenbee.cn'; + +export function resolveCDN(input: string | undefined): string { + const v = (input ?? '').trim(); + if (!v) return DEFAULT_CDN_BASE; + return v.replace(/\/+$/, ''); +} diff --git a/src/utils/errors.test.ts b/src/utils/errors.test.ts new file mode 100644 index 0000000..459ee31 --- /dev/null +++ b/src/utils/errors.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { InterruptedError, UnsupportedPlatformError } from './errors.js'; + +describe('errors', () => { + it('InterruptedError carries name and message', () => { + const e = new InterruptedError(); + expect(e.name).toBe('InterruptedError'); + expect(e.message).toBe('interrupted'); + expect(e).toBeInstanceOf(Error); + }); + + it('UnsupportedPlatformError reports os/arch', () => { + const e = new UnsupportedPlatformError('win32', 'x64'); + expect(e.name).toBe('UnsupportedPlatformError'); + expect(e.message).toContain('win32'); + expect(e.message).toContain('x64'); + }); +}); diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..d67e71b --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,18 @@ +export class InterruptedError extends Error { + constructor() { + super('interrupted'); + this.name = 'InterruptedError'; + } +} + +export class UnsupportedPlatformError extends Error { + constructor( + public readonly os: string, + public readonly arch: string, + ) { + super( + `当前平台 (${os}/${arch}) 不支持 Claude Code 自动下载。\n支持的平台: darwin-arm64, darwin-x64, linux-arm64, linux-x64, linux-arm64-musl, linux-x64-musl\n请手动安装。`, + ); + this.name = 'UnsupportedPlatformError'; + } +} diff --git a/src/utils/json-merge.test.ts b/src/utils/json-merge.test.ts new file mode 100644 index 0000000..45588df --- /dev/null +++ b/src/utils/json-merge.test.ts @@ -0,0 +1,57 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { mergeJSONFile } from './json-merge.js'; + +describe('mergeJSONFile', () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'json-merge-')); + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it('creates a new file when missing', async () => { + const p = join(dir, 'a.json'); + await mergeJSONFile(p, (m) => { + m.foo = 1; + }); + const data = JSON.parse(readFileSync(p, 'utf8')); + expect(data).toEqual({ foo: 1 }); + }); + + it('merges into existing object preserving other keys', async () => { + const p = join(dir, 'b.json'); + writeFileSync(p, JSON.stringify({ a: 1, b: 2 })); + await mergeJSONFile(p, (m) => { + m.b = 22; + m.c = 3; + }); + const data = JSON.parse(readFileSync(p, 'utf8')); + expect(data).toEqual({ a: 1, b: 22, c: 3 }); + }); + + it('overwrites when existing file has invalid JSON', async () => { + const p = join(dir, 'c.json'); + writeFileSync(p, '{not json'); + await mergeJSONFile(p, (m) => { + m.x = 1; + }); + const data = JSON.parse(readFileSync(p, 'utf8')); + expect(data).toEqual({ x: 1 }); + }); + + it('writes pretty-printed JSON with trailing newline', async () => { + const p = join(dir, 'd.json'); + await mergeJSONFile(p, (m) => { + m.foo = 'bar'; + }); + const raw = readFileSync(p, 'utf8'); + expect(raw.endsWith('\n')).toBe(true); + expect(raw).toContain(' "foo": "bar"'); + }); +}); diff --git a/src/utils/json-merge.ts b/src/utils/json-merge.ts new file mode 100644 index 0000000..b1d6fe2 --- /dev/null +++ b/src/utils/json-merge.ts @@ -0,0 +1,26 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +export async function mergeJSONFile( + path: string, + apply: (m: Record) => void, +): Promise { + await mkdir(dirname(path), { recursive: true }); + let existing: Record = {}; + try { + const raw = await readFile(path, 'utf8'); + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + existing = parsed as Record; + } + } catch { + process.stderr.write(`warning: ${path} 不是合法 JSON, 将覆盖\n`); + } + } catch { + // file missing — start fresh + } + apply(existing); + const out = `${JSON.stringify(existing, null, 2)}\n`; + await writeFile(path, out, 'utf8'); +} diff --git a/src/utils/paths.test.ts b/src/utils/paths.test.ts new file mode 100644 index 0000000..b93e6ab --- /dev/null +++ b/src/utils/paths.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest'; +import { binDir, claudeBinPath, claudeJsonPath, claudeSettingsPath, stateDir } from './paths.js'; + +describe('paths', () => { + it('stateDir returns ~/.claude-code-cn', () => { + vi.stubEnv('HOME', '/tmp/test-home'); + expect(stateDir()).toBe('/tmp/test-home/.claude-code-cn'); + vi.unstubAllEnvs(); + }); + + it('binDir returns stateDir/bin', () => { + vi.stubEnv('HOME', '/tmp/test-home'); + expect(binDir()).toBe('/tmp/test-home/.claude-code-cn/bin'); + vi.unstubAllEnvs(); + }); + + it('claudeBinPath returns binDir/claude', () => { + vi.stubEnv('HOME', '/tmp/test-home'); + expect(claudeBinPath()).toBe('/tmp/test-home/.claude-code-cn/bin/claude'); + vi.unstubAllEnvs(); + }); + + it('claudeSettingsPath returns ~/.claude/settings.json', () => { + vi.stubEnv('HOME', '/tmp/test-home'); + expect(claudeSettingsPath()).toBe('/tmp/test-home/.claude/settings.json'); + vi.unstubAllEnvs(); + }); + + it('claudeJsonPath returns ~/.claude.json', () => { + vi.stubEnv('HOME', '/tmp/test-home'); + expect(claudeJsonPath()).toBe('/tmp/test-home/.claude.json'); + vi.unstubAllEnvs(); + }); +}); diff --git a/src/utils/paths.ts b/src/utils/paths.ts new file mode 100644 index 0000000..ff7378b --- /dev/null +++ b/src/utils/paths.ts @@ -0,0 +1,22 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +export function stateDir(): string { + return join(homedir(), '.claude-code-cn'); +} + +export function binDir(): string { + return join(stateDir(), 'bin'); +} + +export function claudeBinPath(): string { + return join(binDir(), 'claude'); +} + +export function claudeSettingsPath(): string { + return join(homedir(), '.claude', 'settings.json'); +} + +export function claudeJsonPath(): string { + return join(homedir(), '.claude.json'); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f5bf3e3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..66b5b7e --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { cli: 'src/cli.ts' }, + format: ['esm'], + target: 'node18', + platform: 'node', + clean: true, + dts: false, + sourcemap: true, + banner: { js: '#!/usr/bin/env node' }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..14839f3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/cli.ts'], + thresholds: { + lines: 85, + functions: 80, + branches: 80, + statements: 85, + }, + }, + }, +});