初版:中文交互式 Coding CLI 装机与订阅助手(体检/安装/状态/订阅切换)

This commit is contained in:
万能助手
2026-09-01 09:12:51 +08:00
commit 4dd3d1af78
12 changed files with 1883 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.DS_Store
+83
View File
@@ -0,0 +1,83 @@
# Coding Helper(cch)
中文交互式 Coding CLI 装机与订阅助手。一个命令完成:环境体检修复 → 安装编码工具 → 检测订阅状态 → 切换第三方端点。
## 功能
| 功能 | 说明 |
| --- | --- |
| 🏥 环境体检 | 检测 Node/npm/Git/curl/网络/目录权限,缺失项一键调用系统包管理器修复 |
| 📦 软件广场 | 交互式安装 / 更新 8 款编码工具,自动按平台(Windows/macOS/Linux)选择渠道 |
| 📊 状态总览 | 一览各工具:是否安装、版本、官方订阅 / 第三方端点 / 未登录 |
| 🔑 订阅管理 | 第三方 API 端点预设(新增 / 删除 / 切换 / 恢复官方),切换前自动备份原配置 |
## 支持的工具
| 工具 | 安装 | 第三方端点切换 |
| --- | --- | --- |
| Claude Code | npm / winget / 官方脚本 | ✅ settings.json env 块 |
| Codex CLI | npm / 官方脚本 | ✅ config.toml + auth.json |
| OpenCode | npm / brew / choco / scoop / 官方脚本 | ✅ opencode.json provider.baseURL |
| Crush | npm / winget / brew / apt | 🧪 实验性(crushrc export 注入) |
| Kimi Code CLI | 官方脚本 | — |
| Qwen Code | npm | — |
| CodeBuddy Code | npm | — |
| Cursor CLI | 官方脚本 | — |
## 使用
```bash
# 交互模式(推荐)
npx coding-helper
# 或安装后使用短命令
npm install -g coding-helper
cch
# 非交互(脚本 / CI)
cch doctor # 环境体检,打印结果,缺关键依赖时退出码 1
cch status # 打印各工具安装与订阅状态
```
## 订阅切换说明
- 预设保存在 `~/.cch/presets.json`,切换时原配置备份为 `*.cch.bak`,可随时在「恢复官方配置」还原
- Codex 切换会以 API Key 模式覆盖 ChatGPT 订阅登录(auth.json 同样有备份)
- OpenCode 的 API Key 为明文写入 opencode.json,注意文件权限
- 各家 CLI 升级可能改变配置格式,如切换后异常,先执行「恢复官方配置」
## 开发
```bash
npm install
npm run dev # tsx 直接运行
npm run build # 编译到 dist/
npm run typecheck
```
### 架构
```text
src/
├── index.ts # 入口:交互主菜单 + 非交互子命令
├── run.ts # 跨平台命令执行(Windows 自动 shell、脚本安装封装)
├── platform.ts # 平台 / 架构 / WSL / 发行版 / 包管理器探测
├── registry.ts # 工具注册表(数据驱动:新增工具只需追加配置)
├── doctor.ts # 环境体检 + 修复命令生成
├── status.ts # 安装 / 版本 / 授权状态探测
└── subscribe.ts # 预设 CRUD + 四工具切换实现(备份 + 原子写)
```
- 技术栈:Node.js ≥ 18 + TypeScript + @clack/prompts(交互 UI)+ picocolors
- 全中文文案;Windows 走 PowerShell(irm | iex),macOS/Linux 走 curl | bash
## 路线图
- [ ] Windows 真机验证(路径 / PowerShell 提权)
- [ ] Crush 端点级配置支持(provider 段,替换当前实验性 export 方案)
- [ ] Kimi / Qwen / CodeBuddy / Cursor 的第三方端点支持(视官方能力)
- [ ] 一键更新全部已装工具
- [ ] 代理 / 镜像(npm registry、安装脚本)配置项
## 许可
MIT
+641
View File
@@ -0,0 +1,641 @@
{
"name": "coding-helper",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "coding-helper",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.0.0",
"picocolors": "^1.1.1"
},
"bin": {
"cch": "dist/index.js"
},
"devDependencies": {
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@clack/core": {
"version": "1.4.3",
"resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.4.3.tgz",
"integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
"license": "MIT",
"dependencies": {
"fast-wrap-ansi": "^0.2.0",
"sisteransi": "^1.0.5"
},
"engines": {
"node": ">= 20.12.0"
}
},
"node_modules/@clack/prompts": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.7.0.tgz",
"integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
"license": "MIT",
"dependencies": {
"@clack/core": "1.4.3",
"fast-string-width": "^3.0.2",
"fast-wrap-ansi": "^0.2.0",
"sisteransi": "^1.0.5"
},
"engines": {
"node": ">= 20.12.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/fast-string-truncated-width": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
"integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
"license": "MIT"
},
"node_modules/fast-string-width": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/fast-string-width/-/fast-string-width-3.0.2.tgz",
"integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
"license": "MIT",
"dependencies": {
"fast-string-truncated-width": "^3.0.2"
}
},
"node_modules/fast-wrap-ansi": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
"integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
"license": "MIT",
"dependencies": {
"fast-string-width": "^3.0.2"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
"node_modules/tsx": {
"version": "4.23.13",
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.23.13.tgz",
"integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
}
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "coding-helper",
"version": "0.1.0",
"description": "跨平台中文交互式 Coding CLI 助手:一键安装 / 环境体检修复 / 状态检测 / 第三方订阅切换",
"type": "module",
"bin": {
"cch": "dist/index.js"
},
"main": "dist/index.js",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"keywords": [
"cli",
"coding",
"helper",
"installer",
"claude-code",
"codex",
"opencode"
],
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.0.0",
"picocolors": "^1.1.1"
},
"devDependencies": {
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}
+142
View File
@@ -0,0 +1,142 @@
import os from 'node:os'
import fs from 'node:fs'
import { commandExists, run } from './run.js'
import type { PlatformInfo, PackageManager } from './platform.js'
export interface EnvCheckItem {
id: string
label: string
ok: boolean
detail: string
/** 修复命令(数组形式),无法自动修复时为 null */
fix: string[] | null
fixNote: string
}
/** 环境体检:检测编码工具运行所需的基础环境 */
export async function checkEnv(pf: PlatformInfo): Promise<EnvCheckItem[]> {
const items: EnvCheckItem[] = []
// Node 版本
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
items.push({
id: 'node',
label: 'Node.js 运行时',
ok: nodeMajor >= 18,
detail: `v${process.versions.node}${nodeMajor >= 18 ? '' : '(需 ≥ 18)'}`,
fix: nodeMajor >= 18 ? null : fixCommand(pf, 'node'),
fixNote: '安装/升级 Node.js LTS',
})
// npm
const npmOk = commandExists('npm')
const npmVer = npmOk ? run('npm', ['--version'], 15_000).stdout.trim() : ''
items.push({
id: 'npm',
label: 'npm 包管理器',
ok: npmOk,
detail: npmOk ? `v${npmVer}` : '未找到(多数工具经 npm 安装)',
fix: npmOk ? null : fixCommand(pf, 'node'),
fixNote: '随 Node.js 一并安装',
})
// git
const gitOk = commandExists('git')
const gitVer = gitOk ? run('git', ['--version'], 15_000).stdout.trim() : ''
items.push({
id: 'git',
label: 'Git',
ok: gitOk,
detail: gitOk ? gitVer : '未找到(版本管理与部分安装脚本依赖)',
fix: gitOk ? null : fixCommand(pf, 'git'),
fixNote: '安装 Git',
})
// curl(Windows 用 PowerShell,不需要 curl)
if (pf.platform !== 'windows') {
const curlOk = commandExists('curl')
items.push({
id: 'curl',
label: 'curl',
ok: curlOk,
detail: curlOk ? '可用' : '未找到(官方安装脚本依赖)',
fix: curlOk ? null : fixCommand(pf, 'curl'),
fixNote: '安装 curl',
})
} else {
const psOk = commandExists('powershell')
items.push({
id: 'powershell',
label: 'PowerShell',
ok: psOk,
detail: psOk ? '可用(用于官方脚本安装)' : '未找到',
fix: null,
fixNote: 'Windows 10+ 自带,请手动排查',
})
}
// 网络(探测 npm 当前 registry,兼顾国内镜像)
const registry = (() => {
const r = run('npm', ['config', 'get', 'registry'], 10_000)
return /^https?:\/\//.test(r.stdout.trim()) ? r.stdout.trim().replace(/\/$/, '') : 'https://registry.npmjs.org'
})()
const netOk = await checkNetwork(registry)
items.push({
id: 'network',
label: `网络连通性(${registry.replace(/^https?:\/\//, '')})`,
ok: netOk,
detail: netOk ? '正常' : '不可达(安装将失败,请检查网络或代理)',
fix: null,
fixNote: '无法自动修复,请检查网络 / HTTP(S)_PROXY 设置',
})
// 家目录可写
const HOME = os.homedir()
let homeOk = false
try {
fs.accessSync(HOME, fs.constants.W_OK)
homeOk = true
} catch {
homeOk = false
}
items.push({
id: 'home',
label: '用户目录可写',
ok: homeOk,
detail: homeOk ? HOME : `${HOME} 不可写`,
fix: null,
fixNote: '请检查用户目录权限',
})
return items
}
async function checkNetwork(baseUrl: string): Promise<boolean> {
try {
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), 6_000)
const res = await fetch(baseUrl, { signal: ctrl.signal })
clearTimeout(timer)
return res.ok
} catch {
return false
}
}
/** 生成某依赖在当前平台的安装命令 */
function fixCommand(pf: PlatformInfo, what: 'node' | 'git' | 'curl'): string[] | null {
const pm: PackageManager | undefined = pf.packageManagers[0]
const pkg: Record<string, Record<string, string>> = {
winget: { node: 'OpenJS.NodeJS.LTS', git: 'Git.Git', curl: 'cURL.cURL' },
scoop: { node: 'nodejs-lts', git: 'git', curl: 'curl' },
choco: { node: 'nodejs-lts', git: 'git', curl: 'curl' },
brew: { node: 'node@22', git: 'git', curl: 'curl' },
apt: { node: 'nodejs', git: 'git', curl: 'curl' },
dnf: { node: 'nodejs', git: 'git', curl: 'curl' },
pacman: { node: 'nodejs', git: 'git', curl: 'curl' },
}
if (!pm) return null
const p = pkg[pm.id]?.[what]
if (!p) return null
return [...pm.installPrefix, p]
}
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env node
import * as p from '@clack/prompts'
import color from 'picocolors'
import { detectPlatform, type PlatformInfo } from './platform.js'
import { TOOLS, channelsFor, getTool, type ToolDef } from './registry.js'
import { checkEnv } from './doctor.js'
import { checkAll, checkTool } from './status.js'
import {
loadPresets, upsertPreset, deletePreset, applyPreset, restoreOfficial,
type Preset,
} from './subscribe.js'
import { runLive, scriptInstallArgs, commandExists } from './run.js'
const VERSION = '0.1.0'
const pf: PlatformInfo = detectPlatform()
// ---------- 工具函数 ----------
function guard<T>(v: T | symbol): T {
if (p.isCancel(v)) {
p.cancel('已取消')
process.exit(0)
}
return v as T
}
function banner(): void {
const line = color.cyan('◈') + color.dim('─'.repeat(38))
console.log(line)
console.log(` ${color.bold(color.cyan('Coding Helper'))} ${color.dim(`v${VERSION}`)}`)
console.log(` ${color.dim('中文交互式 Coding CLI 装机与订阅助手')}`)
console.log(line)
console.log(` 平台:${pf.platformLabel} 架构:${pf.arch}${pf.isWsl ? '(WSL)' : ''}`)
console.log(` 包管理器:${pf.packageManagers.length ? pf.packageManagers.map((m) => m.name).join('、') : color.yellow('未检测到')}`)
console.log('')
}
const SYM = { ok: color.green('√'), no: color.red('×'), warn: color.yellow('!') }
// ---------- 1. 环境体检 ----------
async function doctorMenu(): Promise<void> {
const s = p.spinner()
s.start('正在体检系统环境…')
const items = await checkEnv(pf)
s.stop('体检完成')
const lines = items
.map((i) => `${i.ok ? SYM.ok : i.fix ? SYM.no : SYM.warn} ${i.label}:${color.dim(i.detail)}`)
.join('\n')
p.note(lines, '环境体检结果')
const broken = items.filter((i) => !i.ok && i.fix)
if (broken.length === 0) {
p.log.success('基础环境完备,无需修复')
return
}
const doFix = guard(await p.confirm({ message: `发现 ${broken.length} 项可自动修复,是否立即执行?` }))
if (!doFix) return
for (const item of broken) {
p.log.step(`修复 ${item.label}:${item.fixNote}`)
const code = await runLive(item.fix![0], item.fix!.slice(1))
if (code === 0) p.log.success(`${item.label} 修复完成`)
else p.log.error(`${item.label} 修复失败(退出码 ${code}),请手动处理`)
}
}
// ---------- 2. 软件广场(安装) ----------
async function storeMenu(): Promise<void> {
const s = p.spinner()
s.start('正在检测安装状态…')
const statuses = checkAll()
s.stop('检测完成')
const options = statuses.map((st) => ({
value: st.tool.id,
label: `${st.installed ? SYM.ok : SYM.no} ${st.tool.name} ${color.dim(`· ${st.tool.vendor}`)}`,
hint: st.installed ? `已装 ${st.version ? 'v' + st.version : ''}` : '未安装',
}))
const toolId = guard(await p.select({ message: '选择要安装/更新的工具(回车确认)', options }))
await installTool(getTool(toolId)!)
}
async function installTool(tool: ToolDef): Promise<void> {
p.log.info(`${tool.name}:${tool.desc}`)
const channels = channelsFor(tool, pf.platform)
if (channels.length === 0) {
p.log.error(`当前平台(${pf.platformLabel})暂无可用安装渠道`)
return
}
const ch = guard(
await p.select({
message: '选择安装渠道',
options: channels.map((c) => ({ value: c.id, label: c.id, hint: c.desc })),
}),
)
const channel = channels.find((c) => c.id === ch)!
if (channel.kind === 'script' && channel.scriptUrl) {
const { cmd, args } = scriptInstallArgs(channel.scriptUrl)
p.log.step(`执行官方脚本:${channel.scriptUrl}`)
const code = await runLive(cmd, args)
if (code === 0) p.log.success('脚本执行完成')
else p.log.error(`脚本执行失败(退出码 ${code})`)
} else if (channel.cmd) {
p.log.step(`执行:${channel.cmd.join(' ')}`)
const code = await runLive(channel.cmd[0], channel.cmd.slice(1))
if (code === 0) p.log.success('安装命令执行完成')
else p.log.error(`安装失败(退出码 ${code}),请检查上方输出`)
}
// 安装后验证
const st = checkTool(tool)
if (st.installed) {
p.log.success(`验证通过:${tool.name} 已可用${st.version ? `(v${st.version})` : ''}`)
p.log.info(`授权提示:${tool.authHint}`)
} else {
p.log.warn('未检测到命令,可能需要重开终端使 PATH 生效')
}
}
// ---------- 3. 状态总览 ----------
function statusView(): void {
const statuses = checkAll()
const authLabel: Record<string, string> = {
official: color.green('官方订阅'),
'third-party': color.magenta('第三方端点'),
unknown: color.dim('未知'),
none: color.dim('未登录'),
}
const rows = statuses
.map((st) => {
const install = st.installed
? color.green(`已装${st.version ? ' v' + st.version : ''}`)
: color.red('未安装')
return `${st.installed ? SYM.ok : SYM.no} ${st.tool.name.padEnd(16, ' ')} ${install.padEnd(22, ' ')} ${authLabel[st.auth]} ${color.dim(st.authDetail)}`
})
.join('\n')
p.note(rows, '工具状态总览')
}
// ---------- 4. 订阅管理 ----------
const SWITCHABLE = TOOLS.filter((t) => t.subscription.supported)
async function subscriptionMenu(): Promise<void> {
const action = guard(
await p.select({
message: '订阅管理',
options: [
{ value: 'list', label: '查看预设' },
{ value: 'add', label: '新增预设' },
{ value: 'apply', label: '切换:应用预设到工具' },
{ value: 'restore', label: '恢复官方配置' },
],
}),
)
const presets = loadPresets()
if (action === 'list') {
if (presets.length === 0) {
p.log.info('暂无预设,先「新增预设」录入第三方端点')
return
}
const rows = presets
.map((x) => `${color.bold(x.name)} ${color.dim(x.baseUrl)}${x.note ? ' ' + color.dim('(' + x.note + ')') : ''}`)
.join('\n')
p.note(rows, `第三方端点预设(${presets.length} 个,存储于 ~/.cch/presets.json)`)
const del = guard(await p.confirm({ message: '是否删除某个预设?', initialValue: false }))
if (!del) return
const id = guard(await p.select({ message: '选择要删除的预设', options: presets.map((x) => ({ value: x.id, label: x.name })) }))
deletePreset(id)
p.log.success('已删除')
return
}
if (action === 'add') {
const name = guard(await p.text({ message: '预设名称(如:某某中转站)', validate: (v) => (v?.trim() ? undefined : '必填') }))!.trim()
const baseUrl = guard(
await p.text({
message: 'Base URL(如 https://api.example.com)',
validate: (v) => (/^https?:\/\/.+/.test(v?.trim() ?? '') ? undefined : '需以 http(s):// 开头'),
}),
)!.trim()
const apiKey = guard(await p.password({ message: 'API Key(输入隐藏,留空跳过)' })) ?? ''
const note = guard(await p.text({ message: '备注(可空)' })) ?? ''
upsertPreset({ name, baseUrl, apiKey: apiKey.trim(), note: note.trim() })
p.log.success(`预设「${name}」已保存(~/.cch/presets.json)`)
return
}
if (action === 'apply') {
if (presets.length === 0) {
p.log.warn('暂无预设,请先新增')
return
}
const preset = guard(
await p.select({ message: '选择要应用的预设', options: presets.map((x) => ({ value: x.id, label: x.name, hint: x.baseUrl })) }),
)
const targets = guard(
await p.multiselect({
message: '应用到哪些工具(空格选择,回车确认)',
options: SWITCHABLE.map((t) => ({
value: t.id,
label: `${t.name}${t.subscription.level === 'experimental' ? color.yellow('(实验性)') : ''}`,
hint: t.subscription.note,
})),
required: true,
}),
)
const target = presets.find((x) => x.id === preset)!
for (const id of targets) {
const r = applyPreset(id, target)
if (r.ok) p.log.success(`${getTool(id)!.name}:${r.message}`)
else p.log.error(`${getTool(id)!.name}:${r.message}`)
}
p.log.info('原配置已备份为 *.cch.bak,可随时在「恢复官方配置」中还原')
return
}
if (action === 'restore') {
const id = guard(
await p.select({
message: '选择要恢复官方配置的工具',
options: SWITCHABLE.map((t) => ({ value: t.id, label: t.name })),
}),
)
const r = restoreOfficial(id)
if (r.ok) p.log.success(`${getTool(id)!.name}:${r.message}`)
else p.log.error(`${getTool(id)!.name}:${r.message}`)
return
}
}
// ---------- 非交互模式(测试 / 脚本用) ----------
function nonInteractive(arg: string): void {
if (arg === 'doctor') {
checkEnv(pf).then((items) => {
for (const i of items) {
console.log(`${i.ok ? '√' : '×'} ${i.label}:${i.detail}${!i.ok && i.fix ? `(修复:${i.fix.join(' ')})` : ''}`)
}
process.exit(items.every((i) => i.ok || !i.fix) ? 0 : 1)
})
return
}
if (arg === 'status') {
for (const st of checkAll()) {
console.log(`${st.installed ? '√' : '×'} ${st.tool.name}:${st.installed ? `已装${st.version ? ' v' + st.version : ''}` : '未安装'}|${st.auth}|${st.authDetail}`)
}
return
}
if (arg === 'version' || arg === '--version' || arg === '-v') {
console.log(VERSION)
return
}
console.log('用法:cch [doctor|status|version](无参数进入交互模式)')
}
// ---------- 主流程 ----------
async function main(): Promise<void> {
const arg = process.argv[2]
if (arg) {
nonInteractive(arg)
return
}
banner()
p.intro(color.cyan('cch'))
for (;;) {
const action = guard(
await p.select({
message: '主菜单',
options: [
{ value: 'doctor', label: '🏥 环境体检', hint: '检测缺失依赖,支持一键修复' },
{ value: 'store', label: '📦 软件广场', hint: '安装 / 更新 8 款编码工具' },
{ value: 'status', label: '📊 状态总览', hint: '安装 / 版本 / 订阅状态一览' },
{ value: 'subscribe', label: '🔑 订阅管理', hint: '第三方端点预设与切换(claude code / codex / opencode / crush)' },
{ value: 'exit', label: '🚪 退出' },
],
}),
)
if (action === 'exit') break
if (action === 'doctor') await doctorMenu()
if (action === 'store') await storeMenu()
if (action === 'status') statusView()
if (action === 'subscribe') await subscriptionMenu()
}
p.outro(color.dim('再见,祝编码愉快'))
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+92
View File
@@ -0,0 +1,92 @@
import os from 'node:os'
import { run } from './run.js'
export type Platform = 'windows' | 'macos' | 'linux'
export interface PlatformInfo {
platform: Platform
platformLabel: string
arch: string
osVersion: string
isWsl: boolean
/** 按优先级排列的可用包管理器 */
packageManagers: PackageManager[]
/** Linux 发行版(仅 linux) */
distro: string | null
}
export interface PackageManager {
id: 'winget' | 'choco' | 'scoop' | 'brew' | 'apt' | 'dnf' | 'pacman'
name: string
/** 安装包的命令前缀,如 ['sudo', 'apt-get', 'install', '-y'] */
installPrefix: string[]
needsSudo: boolean
}
export function detectPlatform(): PlatformInfo {
const platform: Platform =
process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux'
const isWsl =
platform === 'linux' && /microsoft|WSL/i.test(os.release()) && os.release().toLowerCase().includes('microsoft')
const distro = platform === 'linux' ? detectDistro() : null
return {
platform,
platformLabel: { windows: 'Windows', macos: 'macOS', linux: distro ? `Linux (${distro})` : 'Linux' }[platform],
arch: process.arch,
osVersion: os.release(),
isWsl,
packageManagers: detectPackageManagers(platform),
distro,
}
}
function detectDistro(): string | null {
const r = run('/bin/sh', ['-c', '. /etc/os-release 2>/dev/null && echo "$ID"'], 5_000)
const id = r.stdout.trim()
if (id) return id
return run('/bin/sh', ['-c', 'command -v apk >/dev/null && echo alpine'], 5_000).stdout.trim() || null
}
const PM_DEFS: Record<string, Omit<PackageManager, 'installPrefix' | 'needsSudo'> & { installPrefix: string[]; needsSudo: boolean }> = {
winget: { id: 'winget', name: 'winget', installPrefix: ['winget', 'install'], needsSudo: false },
choco: { id: 'choco', name: 'Chocolatey', installPrefix: ['choco', 'install', '-y'], needsSudo: true },
scoop: { id: 'scoop', name: 'Scoop', installPrefix: ['scoop', 'install'], needsSudo: false },
brew: { id: 'brew', name: 'Homebrew', installPrefix: ['brew', 'install'], needsSudo: false },
apt: { id: 'apt', name: 'apt', installPrefix: ['sudo', 'apt-get', 'install', '-y'], needsSudo: true },
dnf: { id: 'dnf', name: 'dnf', installPrefix: ['sudo', 'dnf', 'install', '-y'], needsSudo: true },
pacman: { id: 'pacman', name: 'pacman', installPrefix: ['sudo', 'pacman', '-S', '--noconfirm'], needsSudo: true },
}
const PM_ORDER: Record<Platform, PackageManager['id'][]> = {
windows: ['winget', 'scoop', 'choco'],
macos: ['brew'],
linux: ['apt', 'dnf', 'pacman'],
}
function detectPackageManagers(platform: Platform): PackageManager[] {
const found: PackageManager[] = []
for (const id of PM_ORDER[platform]) {
const probe = { winget: 'winget', choco: 'choco', scoop: 'scoop', brew: 'brew', apt: 'apt-get', dnf: 'dnf', pacman: 'pacman' }[id]
const ok = process.platform === 'win32'
? run('where.exe', [probe], 8_000).stdout.trim().length > 0
: run('/bin/sh', ['-c', `command -v ${probe}`], 8_000).ok
if (ok) found.push(PM_DEFS[id])
}
return found
}
/** 展开 ~ 为家目录 */
export function home(p: string): string {
if (p === '~') return os.homedir()
if (p.startsWith('~/') || p.startsWith('~\\')) {
return joinPath(os.homedir(), p.slice(2))
}
return p
}
export function joinPath(...parts: string[]): string {
const sep = process.platform === 'win32' ? '\\' : '/'
return parts.join(sep)
}
+180
View File
@@ -0,0 +1,180 @@
import type { Platform } from './platform.js'
export interface InstallChannel {
id: string
platforms: Platform[]
kind: 'command' | 'script'
/** kind=command 时的命令 */
cmd?: string[]
/** kind=script 时的脚本地址(Windows 用 irm|iex,Unix 用 curl|bash) */
scriptUrl?: string
desc: string
}
export interface ToolDef {
id: string
name: string
vendor: string
desc: string
exe: string
versionArgs: string[]
channels: InstallChannel[]
/** 订阅切换支持:null 表示不支持第三方端点切换 */
subscription: {
supported: boolean
level: 'full' | 'experimental'
note: string
}
/** 登录/授权检测提示 */
authHint: string
}
/**
* 工具注册表(数据驱动:新增工具 = 追加一条配置,不改代码)
* 数据来源:官方文档 + Agent-Tools/adapters 调研底稿(2026-08)
*/
export const TOOLS: ToolDef[] = [
{
id: 'claude-code',
name: 'Claude Code',
vendor: 'Anthropic',
desc: 'Anthropic 官方终端编码代理',
exe: 'claude',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', '@anthropic-ai/claude-code'], desc: 'npm 全局安装(需 Node 18+)' },
{ id: 'winget', platforms: ['windows'], kind: 'command', cmd: ['winget', 'install', 'Anthropic.ClaudeCode'], desc: 'winget 安装' },
{ id: 'script', platforms: ['windows'], kind: 'script', scriptUrl: 'https://claude.ai/install.ps1', desc: '官方 PowerShell 脚本' },
{ id: 'script', platforms: ['macos', 'linux'], kind: 'script', scriptUrl: 'https://claude.ai/install.sh', desc: '官方 Shell 脚本' },
],
subscription: {
supported: true,
level: 'full',
note: '写入 ~/.claude/settings.json 的 env 块(ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN)',
},
authHint: '运行 claude 按提示浏览器登录,或用本助手切换第三方端点',
},
{
id: 'codex',
name: 'Codex CLI',
vendor: 'OpenAI',
desc: 'OpenAI 官方终端编码代理',
exe: 'codex',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', '@openai/codex'], desc: 'npm 全局安装' },
{ id: 'script', platforms: ['windows'], kind: 'script', scriptUrl: 'https://chatgpt.com/codex/install.ps1', desc: '官方 PowerShell 脚本' },
{ id: 'script', platforms: ['macos', 'linux'], kind: 'script', scriptUrl: 'https://chatgpt.com/codex/install.sh', desc: '官方 Shell 脚本' },
],
subscription: {
supported: true,
level: 'full',
note: '写入 ~/.codex/config.toml(openai_base_url)与 auth.json(OPENAI_API_KEY)',
},
authHint: '运行 codex login 登录 ChatGPT 账号(需 Plus/Pro 等计划)',
},
{
id: 'opencode',
name: 'OpenCode',
vendor: 'SST / 开源社区',
desc: '开源 AI 编码代理,多模型接入',
exe: 'opencode',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', 'opencode-ai'], desc: 'npm 全局安装' },
{ id: 'brew', platforms: ['macos'], kind: 'command', cmd: ['brew', 'install', 'opencode'], desc: 'Homebrew 安装' },
{ id: 'choco', platforms: ['windows'], kind: 'command', cmd: ['choco', 'install', 'opencode', '-y'], desc: 'Chocolatey 安装' },
{ id: 'scoop', platforms: ['windows'], kind: 'command', cmd: ['scoop', 'install', 'opencode'], desc: 'Scoop 安装' },
{ id: 'script', platforms: ['macos', 'linux'], kind: 'script', scriptUrl: 'https://opencode.ai/install', desc: '官方脚本' },
],
subscription: {
supported: true,
level: 'full',
note: '写入 ~/.config/opencode/opencode.json 的 provider.baseURL',
},
authHint: '运行 opencode auth login 授权,或用本助手切换第三方端点',
},
{
id: 'crush',
name: 'Crush',
vendor: 'Charmbracelet',
desc: '高颜值终端 AI 编码代理',
exe: 'crush',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', '@charmland/crush'], desc: 'npm 全局安装' },
{ id: 'winget', platforms: ['windows'], kind: 'command', cmd: ['winget', 'install', 'charmbracelet.crush'], desc: 'winget 安装' },
{ id: 'brew', platforms: ['macos'], kind: 'command', cmd: ['brew', 'install', 'charmbracelet/tap/crush'], desc: 'Homebrew 安装' },
{ id: 'apt', platforms: ['linux'], kind: 'command', cmd: ['sudo', 'apt-get', 'install', '-y', 'crush'], desc: 'apt 安装' },
],
subscription: {
supported: true,
level: 'experimental',
note: '实验性:写入 ~/.config/crush/crushrc(export 注入),端点级配置请参照官方文档核对',
},
authHint: '通过 API Key 环境变量授权',
},
{
id: 'kimi',
name: 'Kimi Code CLI',
vendor: 'Moonshot AI',
desc: '月之暗面 Kimi 终端编码助手',
exe: 'kimi',
versionArgs: ['--version'],
channels: [
{ id: 'script', platforms: ['windows'], kind: 'script', scriptUrl: 'https://code.kimi.com/kimi-code/install.ps1', desc: '官方 PowerShell 脚本' },
{ id: 'script', platforms: ['macos', 'linux'], kind: 'script', scriptUrl: 'https://code.kimi.com/kimi-code/install.sh', desc: '官方 Shell 脚本' },
],
subscription: { supported: false, level: 'full', note: '暂不支持第三方端点切换(官方账号体系)' },
authHint: '运行 kimi 按提示登录 Kimi 账号',
},
{
id: 'qwen',
name: 'Qwen Code',
vendor: '阿里通义',
desc: '通义千问终端编码助手(Gemini CLI 血统)',
exe: 'qwen',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', '@qwen-code/qwen-code'], desc: 'npm 全局安装' },
],
subscription: { supported: false, level: 'full', note: '暂不支持第三方端点切换(DASHSCOPE_API_KEY 官方体系)' },
authHint: '设置 DASHSCOPE_API_KEY 环境变量后使用',
},
{
id: 'codebuddy',
name: 'CodeBuddy Code',
vendor: '腾讯',
desc: '腾讯 CodeBuddy 终端编码代理',
exe: 'codebuddy',
versionArgs: ['--version'],
channels: [
{ id: 'npm', platforms: ['windows', 'macos', 'linux'], kind: 'command', cmd: ['npm', 'install', '-g', '@tencent-ai/codebuddy-code'], desc: 'npm 全局安装' },
],
subscription: { supported: false, level: 'full', note: '暂不支持第三方端点切换(官方账号体系)' },
authHint: '运行 codebuddy 按提示登录腾讯账号',
},
{
id: 'cursor',
name: 'Cursor CLI',
vendor: 'Cursor (Anysphere)',
desc: 'Cursor 编辑器的终端 Agent(命令名 agent)',
exe: 'agent',
versionArgs: ['--version'],
channels: [
{ id: 'script', platforms: ['windows'], kind: 'script', scriptUrl: 'https://cursor.com/install?win32=true', desc: '官方 PowerShell 脚本' },
{ id: 'script', platforms: ['macos', 'linux'], kind: 'script', scriptUrl: 'https://cursor.com/install', desc: '官方 Shell 脚本' },
],
subscription: { supported: false, level: 'full', note: '暂不支持第三方端点切换(Cursor 账号体系)' },
authHint: '运行 agent login 登录 Cursor 账号',
},
]
export function getTool(id: string): ToolDef | undefined {
return TOOLS.find((t) => t.id === id)
}
/** 按当前平台过滤出可用的安装渠道 */
export function channelsFor(tool: ToolDef, platform: Platform): InstallChannel[] {
return tool.channels.filter((c) => c.platforms.includes(platform))
}
+61
View File
@@ -0,0 +1,61 @@
import { spawn, spawnSync } from 'node:child_process'
export interface RunResult {
ok: boolean
code: number | null
stdout: string
stderr: string
}
const IS_WIN = process.platform === 'win32'
/** 同步执行命令并捕获输出(Windows 自动走 shell 以支持 npm.cmd 等) */
export function run(cmd: string, args: string[] = [], timeoutMs = 30_000): RunResult {
const r = spawnSync(cmd, args, {
encoding: 'utf8',
timeout: timeoutMs,
shell: IS_WIN,
windowsHide: true,
})
return {
ok: r.status === 0,
code: r.status,
stdout: (r.stdout ?? '').toString(),
stderr: (r.stderr ?? '').toString(),
}
}
/** 异步执行命令并实时透传输出(用于安装等长任务),返回退出码 */
export function runLive(cmd: string, args: string[] = []): Promise<number | null> {
return new Promise((resolve) => {
const child = spawn(cmd, args, {
stdio: 'inherit',
shell: IS_WIN,
windowsHide: true,
})
child.on('error', (err) => {
console.error(`无法启动命令 ${cmd}: ${err.message}`)
resolve(null)
})
child.on('close', (code) => resolve(code))
})
}
/** 检测可执行文件是否在 PATH 中 */
export function commandExists(exe: string): boolean {
if (IS_WIN) {
return run('where.exe', [exe], 10_000).stdout.trim().length > 0
}
return run('/bin/sh', ['-c', `command -v ${JSON.stringify(exe)}`], 10_000).ok
}
/** 平台差异化的脚本安装命令(PowerShell irm / curl|bash) */
export function scriptInstallArgs(url: string): { cmd: string; args: string[] } {
if (IS_WIN) {
return {
cmd: 'powershell',
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', `irm ${url} | iex`],
}
}
return { cmd: 'bash', args: ['-c', `curl -fsSL ${url} | bash`] }
}
+117
View File
@@ -0,0 +1,117 @@
import fs from 'node:fs'
import { commandExists, run } from './run.js'
import { home } from './platform.js'
import { TOOLS, type ToolDef } from './registry.js'
export interface ToolStatus {
tool: ToolDef
installed: boolean
version: string | null
/** 登录/授权状态(尽力而为的探测) */
auth: 'official' | 'third-party' | 'unknown' | 'none'
authDetail: string
}
/** 检测单个工具:安装、版本、授权状态 */
export function checkTool(tool: ToolDef): ToolStatus {
const installed = commandExists(tool.exe)
let version: string | null = null
if (installed) {
const r = run(tool.exe, tool.versionArgs, 20_000)
const m = r.stdout.match(/(\d+\.\d+\.\d+(?:[-.][\w]+)?)/)
version = m ? m[1] : (r.stdout.trim().split('\n')[0] || null)
}
const auth = detectAuth(tool.id)
return { tool, installed, version, auth: auth.state, authDetail: auth.detail }
}
export function checkAll(): ToolStatus[] {
return TOOLS.map(checkTool)
}
interface AuthState {
state: 'official' | 'third-party' | 'unknown' | 'none'
detail: string
}
function exists(p: string): boolean {
try {
fs.accessSync(home(p))
return true
} catch {
return false
}
}
function readText(p: string): string | null {
try {
return fs.readFileSync(home(p), 'utf8')
} catch {
return null
}
}
/**
* 授权状态探测(尽力而为,各家 CLI 升级可能改变文件格式):
* - official: 检测到官方登录凭证
* - third-party: 检测到自定义端点配置
* - none: 未检测到任何凭证
* - unknown: 无法判断
*/
function detectAuth(toolId: string): AuthState {
switch (toolId) {
case 'claude-code': {
const settings = readText('~/.claude/settings.json')
if (settings && /ANTHROPIC_BASE_URL/i.test(settings)) {
return { state: 'third-party', detail: 'settings.json 已配置第三方端点' }
}
if (exists('~/.claude/.credentials.json')) {
return { state: 'official', detail: '已登录官方账号(Pro/Max 等)' }
}
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) {
return { state: 'official', detail: '环境变量已设置 API Key' }
}
return { state: 'none', detail: '未检测到登录凭证' }
}
case 'codex': {
const cfg = readText('~/.codex/config.toml')
if (cfg && /openai_base_url\s*=|model_providers\./.test(cfg)) {
return { state: 'third-party', detail: 'config.toml 已配置自定义端点' }
}
if (exists('~/.codex/auth.json')) {
return { state: 'official', detail: '已登录(ChatGPT 订阅或 API Key)' }
}
return { state: 'none', detail: '未检测到 auth.json' }
}
case 'opencode': {
const cfg = readText('~/.config/opencode/opencode.json')
if (cfg && /baseURL/i.test(cfg)) {
return { state: 'third-party', detail: 'opencode.json 已配置自定义 baseURL' }
}
if (exists('~/.local/share/opencode/auth.json')) {
return { state: 'official', detail: '已保存授权' }
}
return { state: 'none', detail: '未检测到授权文件' }
}
case 'crush': {
const cfg = readText('~/.config/crush/crushrc')
if (cfg && /apiKey|api_key|OPENAI_API_KEY/.test(cfg)) {
return { state: 'third-party', detail: 'crushrc 已配置 API Key' }
}
return { state: 'unknown', detail: '请运行 crush 查看授权状态' }
}
case 'kimi':
return exists('~/.kimi-code') ? { state: 'official', detail: '检测到配置目录' } : { state: 'none', detail: '未检测到配置' }
case 'qwen':
return process.env.DASHSCOPE_API_KEY
? { state: 'official', detail: 'DASHSCOPE_API_KEY 已设置' }
: { state: 'none', detail: '未设置 DASHSCOPE_API_KEY' }
case 'codebuddy':
return exists('~/.codebuddy') ? { state: 'official', detail: '检测到配置目录' } : { state: 'none', detail: '未检测到配置' }
case 'cursor':
return exists('~/.cursor/cli-config.json') ? { state: 'official', detail: '检测到 CLI 配置' } : { state: 'none', detail: '未检测到配置' }
default:
return { state: 'unknown', detail: '' }
}
}
+209
View File
@@ -0,0 +1,209 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { home } from './platform.js'
/** 第三方订阅预设 */
export interface Preset {
id: string
name: string
baseUrl: string
apiKey: string
note: string
createdAt: string
}
const PRESETS_DIR = path.join(os.homedir(), '.cch')
const PRESETS_FILE = path.join(PRESETS_DIR, 'presets.json')
const BAK_SUFFIX = '.cch.bak'
export function loadPresets(): Preset[] {
try {
return JSON.parse(fs.readFileSync(PRESETS_FILE, 'utf8'))
} catch {
return []
}
}
export function savePresets(presets: Preset[]): void {
fs.mkdirSync(PRESETS_DIR, { recursive: true })
fs.writeFileSync(PRESETS_FILE, JSON.stringify(presets, null, 2), 'utf8')
}
export function upsertPreset(p: Omit<Preset, 'id' | 'createdAt'> & { id?: string }): Preset {
const presets = loadPresets()
const id = p.id ?? `p_${Date.now().toString(36)}`
const preset: Preset = { ...p, id, createdAt: new Date().toISOString() }
const idx = presets.findIndex((x) => x.id === id)
if (idx >= 0) {
presets[idx] = { ...presets[idx], ...preset }
} else {
presets.push(preset)
}
savePresets(presets)
return preset
}
export function deletePreset(id: string): void {
savePresets(loadPresets().filter((p) => p.id !== id))
}
// ---------- 配置文件安全读写(备份 + 原子写) ----------
function backup(file: string): void {
const f = home(file)
if (fs.existsSync(f)) {
fs.copyFileSync(f, f + BAK_SUFFIX)
}
}
function restore(file: string): boolean {
const f = home(file)
const bak = f + BAK_SUFFIX
if (fs.existsSync(bak)) {
fs.copyFileSync(bak, f)
return true
}
return false
}
function atomicWrite(file: string, content: string): void {
const f = home(file)
fs.mkdirSync(path.dirname(f), { recursive: true })
const tmp = f + '.cch.tmp'
fs.writeFileSync(tmp, content, 'utf8')
fs.renameSync(tmp, f)
}
function readJson(file: string): Record<string, unknown> {
try {
return JSON.parse(fs.readFileSync(home(file), 'utf8'))
} catch {
return {}
}
}
// ---------- 各工具的切换实现 ----------
export interface SwitchResult {
toolId: string
ok: boolean
message: string
}
/** 将预设应用到工具(写配置前自动备份原文件为 *.cch.bak) */
export function applyPreset(toolId: string, preset: Preset): SwitchResult {
switch (toolId) {
case 'claude-code':
return applyClaude(preset)
case 'codex':
return applyCodex(preset)
case 'opencode':
return applyOpencode(preset)
case 'crush':
return applyCrush(preset)
default:
return { toolId, ok: false, message: '该工具暂不支持第三方订阅切换' }
}
}
/** 恢复官方配置(还原备份) */
export function restoreOfficial(toolId: string): SwitchResult {
const files: Record<string, string[]> = {
'claude-code': ['~/.claude/settings.json'],
codex: ['~/.codex/config.toml', '~/.codex/auth.json'],
opencode: ['~/.config/opencode/opencode.json'],
crush: ['~/.config/crush/crushrc'],
}
const list = files[toolId]
if (!list) return { toolId, ok: false, message: '该工具暂不支持恢复操作' }
const restored = list.filter((f) => restore(f))
return restored.length > 0
? { toolId, ok: true, message: `已还原备份:${restored.join('、')}` }
: { toolId, ok: false, message: '未找到切换前的备份(*.cch.bak)' }
}
function applyClaude(p: Preset): SwitchResult {
const file = '~/.claude/settings.json'
backup(file)
const obj = readJson(file) as Record<string, any>
obj.env = obj.env ?? {}
obj.env.ANTHROPIC_BASE_URL = p.baseUrl
if (p.apiKey) obj.env.ANTHROPIC_AUTH_TOKEN = p.apiKey
atomicWrite(file, JSON.stringify(obj, null, 2))
return { toolId: 'claude-code', ok: true, message: `已写入 ${file}(env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN)` }
}
function applyCodex(p: Preset): SwitchResult {
const cfgFile = '~/.codex/config.toml'
backup(cfgFile)
const raw = (() => {
try {
return fs.readFileSync(home(cfgFile), 'utf8')
} catch {
return ''
}
})()
const lines = raw
.split('\n')
.filter((l) => !/^\s*openai_base_url\s*=/.test(l) && !/^\s*# --- cch /.test(l))
lines.push(`# --- cch 第三方订阅 ${p.name} ---`)
lines.push(`openai_base_url = "${p.baseUrl}"`)
atomicWrite(cfgFile, lines.join('\n').replace(/\n{3,}/g, '\n\n') + '\n')
if (p.apiKey) {
const authFile = '~/.codex/auth.json'
backup(authFile)
atomicWrite(authFile, JSON.stringify({ OPENAI_API_KEY: p.apiKey }, null, 2))
}
return {
toolId: 'codex',
ok: true,
message: `已写入 ${cfgFile}(openai_base_url)${p.apiKey ? ' 及 auth.json(API Key 模式,将覆盖 ChatGPT 订阅登录,恢复官方可用「恢复官方配置」)' : ''}`,
}
}
function applyOpencode(p: Preset): SwitchResult {
const file = '~/.config/opencode/opencode.json'
backup(file)
const obj = readJson(file) as Record<string, any>
obj.provider = obj.provider ?? {}
// 同时配置 anthropic / openai 两个 provider 槽位,OpenCode 按模型自动选用
obj.provider.anthropic = { ...(obj.provider.anthropic ?? {}), baseURL: p.baseUrl, apiKey: p.apiKey }
obj.provider.openai = { ...(obj.provider.openai ?? {}), baseURL: p.baseUrl, apiKey: p.apiKey }
atomicWrite(file, JSON.stringify(obj, null, 2))
return {
toolId: 'opencode',
ok: true,
message: `已写入 ${file}(provider.anthropic/openai.baseURL,注意 API Key 为明文存储)`,
}
}
function applyCrush(p: Preset): SwitchResult {
const file = '~/.config/crush/crushrc'
backup(file)
let raw = (() => {
try {
return fs.readFileSync(home(file), 'utf8')
} catch {
return ''
}
})()
raw = raw
.split('\n')
.filter((l) => !/^\s*# --- cch /.test(l) && !/^\s*export\s+(OPENAI_API_KEY|ANTHROPIC_API_KEY)=/.test(l))
.join('\n')
const block = [
`# --- cch 第三方订阅 ${p.name}(实验性) ---`,
`# 端点:${p.baseUrl}`,
`export OPENAI_API_KEY=${p.apiKey}`,
`export OPENAI_BASE_URL=${p.baseUrl}`,
'# 端点级配置如需精细控制,请参照 https://docs.crush.chat 核对 provider 段',
].join('\n')
atomicWrite(file, (raw.trimEnd() ? raw.trimEnd() + '\n' : '') + block + '\n')
return {
toolId: 'crush',
ok: true,
message: `已写入 ${file}(export 注入,实验性:请运行 crush 验证生效)`,
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": false
},
"include": ["src"]
}