初版:中文交互式 Coding CLI 装机与订阅助手(体检/安装/状态/订阅切换)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -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
|
||||
Generated
+641
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
@@ -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
@@ -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
@@ -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
@@ -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: '' }
|
||||
}
|
||||
}
|
||||
@@ -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 验证生效)`,
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user