@cannbot-ai/install-helper 0.0.2-beta.12 → 0.0.2-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -4
- package/dist/index.js +157 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,22 @@ npx @cannbot-ai/install-helper
|
|
|
18
18
|
|
|
19
19
|
交互式向导会自动检测已安装的 AI 编程工具(OpenCode / Claude Code / Trae / Cursor),通过箭头键选择场景即可完成安装。
|
|
20
20
|
|
|
21
|
+
## 主要特性
|
|
22
|
+
|
|
23
|
+
### 核心功能
|
|
24
|
+
- **动态 Skill 发现**:自动扫描仓库中的 SKILL.md 文件,实时发现可用 Skills
|
|
25
|
+
- **智能缓存机制**:扫描结果缓存 24 小时,大幅提升后续命令执行速度
|
|
26
|
+
- **备份与恢复**:安装新插件时自动备份 AGENTS.md,支持一键恢复
|
|
27
|
+
- **参数校验**:`--tool` 和 `--level` 参数支持校验,提供友好的错误提示(支持中英文)
|
|
28
|
+
- **智能记忆**:记住上次选择的工具和安装级别,下次运行自动填充
|
|
29
|
+
|
|
30
|
+
### 安装管理
|
|
31
|
+
- **交互式向导**:箭头键选择,空格切换,回车确认
|
|
32
|
+
- **插件安装**:支持 10 个官方插件,自动处理依赖
|
|
33
|
+
- **Skill 安装**:支持 89 个 Skills,按 11 个类别组织
|
|
34
|
+
- **智能卸载**:根据安装记录精确删除,自动清理空目录
|
|
35
|
+
- **健康检查**:检测安装状态,自动修复常见问题
|
|
36
|
+
|
|
21
37
|
## 命令一览
|
|
22
38
|
|
|
23
39
|
| 命令 | 说明 |
|
|
@@ -168,6 +184,10 @@ install-helper uninstall ops-direct-invoke --tool opencode
|
|
|
168
184
|
|
|
169
185
|
## 可用场景
|
|
170
186
|
|
|
187
|
+
install-helper 支持 **10 个官方插件** 和 **89 个 Skills**(动态发现,数量可能随仓库更新而变化)。
|
|
188
|
+
|
|
189
|
+
#### 官方插件
|
|
190
|
+
|
|
171
191
|
| # | 场景 | ID | 别名 |
|
|
172
192
|
|---|------|----|------|
|
|
173
193
|
| 1 | AscendC Kernel 直调 | ops-direct-invoke | ops-direct, direct, kernel |
|
|
@@ -181,6 +201,23 @@ install-helper uninstall ops-direct-invoke --tool opencode
|
|
|
181
201
|
| 9 | 代码检视 | ops-code-reviewer | code-review, reviewer |
|
|
182
202
|
| 10 | torch.compile 图模式 | torch-compile | torch, compile |
|
|
183
203
|
|
|
204
|
+
#### Skills 类别
|
|
205
|
+
|
|
206
|
+
Skills 按 11 个类别组织(动态发现,数量可能随仓库更新而变化):
|
|
207
|
+
- 知识与参考
|
|
208
|
+
- 环境与工具
|
|
209
|
+
- 调试与诊断
|
|
210
|
+
- 测试与质量
|
|
211
|
+
- AscendC 开发
|
|
212
|
+
- PyPTO 开发
|
|
213
|
+
- TileLang 开发
|
|
214
|
+
- Triton 开发
|
|
215
|
+
- 模型推理优化
|
|
216
|
+
- 图模式
|
|
217
|
+
- 平台工具
|
|
218
|
+
|
|
219
|
+
使用 `install-helper install --list` 查看完整的 Skills 列表。
|
|
220
|
+
|
|
184
221
|
## 前置条件
|
|
185
222
|
|
|
186
223
|
- Node.js >= 18
|
|
@@ -192,17 +229,21 @@ install-helper uninstall ops-direct-invoke --tool opencode
|
|
|
192
229
|
install-helper 是一个编排工具:
|
|
193
230
|
|
|
194
231
|
1. **自动检测**已安装的 AI 编程工具
|
|
195
|
-
2.
|
|
196
|
-
3.
|
|
197
|
-
4.
|
|
232
|
+
2. **动态扫描**仓库中的 SKILL.md 文件,发现可用 Skills
|
|
233
|
+
3. **缓存扫描结果**到 `~/.cannbot/scan-cache.json`(24 小时有效)
|
|
234
|
+
4. **克隆或更新** cannbot-skills 仓库(缓存到 `~/.cannbot/repo/`)
|
|
235
|
+
5. **备份现有配置**(如果检测到已安装的 AGENTS.md)
|
|
236
|
+
6. **调用**各插件的 `init.sh` / `install.sh` 脚本执行实际安装
|
|
237
|
+
7. **追踪**安装状态(`~/.cannbot/config.yaml` + manifest)
|
|
198
238
|
|
|
199
239
|
安装逻辑仍由各插件的 init.sh 脚本负责,install-helper 提供统一的交互式入口和状态管理。
|
|
200
240
|
|
|
201
241
|
## 配置文件
|
|
202
242
|
|
|
203
|
-
配置保存在 `~/.cannbot
|
|
243
|
+
配置保存在 `~/.cannbot/` 目录:
|
|
204
244
|
|
|
205
245
|
```yaml
|
|
246
|
+
# config.yaml - 用户配置
|
|
206
247
|
language: zh_CN # 界面语言
|
|
207
248
|
lastTool: opencode # 上次使用的 AI 工具
|
|
208
249
|
lastLevel: project # 上次安装级别
|
|
@@ -211,6 +252,26 @@ installedPlugins: # 已安装的插件列表
|
|
|
211
252
|
- pypto-op-orchestrator
|
|
212
253
|
```
|
|
213
254
|
|
|
255
|
+
```json
|
|
256
|
+
// scan-cache.json - 扫描缓存(自动生成,24 小时有效)
|
|
257
|
+
{
|
|
258
|
+
"skills": [...],
|
|
259
|
+
"timestamp": 1234567890,
|
|
260
|
+
"repoPath": "/path/to/repo"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
// installs/<plugin>.json - 安装记录(每个插件一个文件)
|
|
266
|
+
{
|
|
267
|
+
"pluginId": "ops-direct-invoke",
|
|
268
|
+
"installTime": "2026-06-23T10:00:00Z",
|
|
269
|
+
"configRoot": "/path/to/config",
|
|
270
|
+
"files": [...],
|
|
271
|
+
"directories": [...]
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
214
275
|
## 开发
|
|
215
276
|
|
|
216
277
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -282,7 +282,38 @@ var zh_CN = {
|
|
|
282
282
|
wizard_step_4_skill_title: "[4/4] \u9009\u62E9\u8981\u5B89\u88C5\u7684 Skills \u2014 {category}",
|
|
283
283
|
install_level_project: "\u5B89\u88C5\u5230\u5F53\u524D\u76EE\u5F55\uFF08\u63A8\u8350\uFF0C\u4EC5\u5F53\u524D\u9879\u76EE\u53EF\u7528\uFF09",
|
|
284
284
|
install_level_global: "\u5B89\u88C5\u5230\u5168\u5C40\uFF08\u6240\u6709\u9879\u76EE\u5171\u4EAB\uFF09",
|
|
285
|
-
wizard_select_mode_title: "\u9009\u62E9\u5B89\u88C5\u7C7B\u578B"
|
|
285
|
+
wizard_select_mode_title: "\u9009\u62E9\u5B89\u88C5\u7C7B\u578B",
|
|
286
|
+
skill_count_format: "{count} \u4E2A Skills",
|
|
287
|
+
result_success_format: "{count} \u6210\u529F",
|
|
288
|
+
result_failed_format: "{count} \u5931\u8D25",
|
|
289
|
+
uninstall_remove_label: "\u79FB\u9664",
|
|
290
|
+
uninstall_remove_file_format: "\u79FB\u9664 {count} \u4E2A\u6587\u4EF6",
|
|
291
|
+
uninstall_remove_dir_format: "{count} \u4E2A\u76EE\u5F55",
|
|
292
|
+
uninstall_clean_empty_dir: "\u6E05\u7406\u7A7A\u76EE\u5F55",
|
|
293
|
+
uninstall_skill_not_installed: "\u672A\u5B89\u88C5",
|
|
294
|
+
available_plugins: "\u53EF\u7528\u63D2\u4EF6",
|
|
295
|
+
available_skills: "\u53EF\u7528 Skills",
|
|
296
|
+
skill_total_count: "\u5171 {count} \u4E2A Skills",
|
|
297
|
+
wizard_confirm_install_format: "\u5373\u5C06\u5B89\u88C5 {count} \u4E2A\u63D2\u4EF6\u5230 {tool}\uFF08{level} \u7EA7\u522B\uFF09\uFF1A",
|
|
298
|
+
wizard_install_path: "\u5B89\u88C5\u8DEF\u5F84",
|
|
299
|
+
update_install_hint: "\u4F7F\u7528 {cmd} \u5B89\u88C5\u63D2\u4EF6",
|
|
300
|
+
hint_select_move: "\u79FB\u52A8",
|
|
301
|
+
hint_select_space: "\u7A7A\u683C",
|
|
302
|
+
hint_select_check: "\u52FE\u9009",
|
|
303
|
+
hint_select_confirm: "\u786E\u8BA4",
|
|
304
|
+
hint_checkbox_suffix: "\u4E0D\u9009\u6309\u56DE\u8F66\u53EF\u8FD4\u56DE",
|
|
305
|
+
doctor_not_installed: "\u672A\u5B89\u88C5",
|
|
306
|
+
doctor_links_valid: "{count} \u4E2A\u8F6F\u94FE\u63A5\u5747\u6709\u6548",
|
|
307
|
+
doctor_broken_links: "{count} \u4E2A\u5931\u6548\u94FE\u63A5",
|
|
308
|
+
doctor_fixed: "\u5DF2\u6E05\u7406 {count} \u4E2A\u5931\u6548\u94FE\u63A5",
|
|
309
|
+
doctor_not_exist: "\u4E0D\u5B58\u5728",
|
|
310
|
+
doctor_created: "\u5DF2\u521B\u5EFA {path}",
|
|
311
|
+
doctor_config_exists: "\u5B58\u5728",
|
|
312
|
+
doctor_config_not_exist: "\u4E0D\u5B58\u5728",
|
|
313
|
+
doctor_result_with_fix: "{warnings} \u4E2A\u8B66\u544A, {errors} \u4E2A\u9519\u8BEF, {fixes} \u9879\u5DF2\u4FEE\u590D",
|
|
314
|
+
doctor_result_without_fix: "{warnings} \u4E2A\u8B66\u544A, {errors} \u4E2A\u9519\u8BEF",
|
|
315
|
+
count_suffix: "\u4E2A",
|
|
316
|
+
try_prompt: "\u5E2E\u6211\u5F00\u53D1\u4E00\u4E2A Abs \u7B97\u5B50"
|
|
286
317
|
};
|
|
287
318
|
var en_US = {
|
|
288
319
|
wizard_title: "CANNBot Install Helper",
|
|
@@ -419,7 +450,38 @@ var en_US = {
|
|
|
419
450
|
wizard_step_4_skill_title: "[4/4] Select Skills to install \u2014 {category}",
|
|
420
451
|
install_level_project: "Install to current directory (recommended, current project only)",
|
|
421
452
|
install_level_global: "Install globally (all projects)",
|
|
422
|
-
wizard_select_mode_title: "Select installation type"
|
|
453
|
+
wizard_select_mode_title: "Select installation type",
|
|
454
|
+
skill_count_format: "{count} Skills",
|
|
455
|
+
result_success_format: "{count} succeeded",
|
|
456
|
+
result_failed_format: "{count} failed",
|
|
457
|
+
uninstall_remove_label: "Removed",
|
|
458
|
+
uninstall_remove_file_format: "Removed {count} files",
|
|
459
|
+
uninstall_remove_dir_format: "{count} directories",
|
|
460
|
+
uninstall_clean_empty_dir: "Cleaned empty directory",
|
|
461
|
+
uninstall_skill_not_installed: "Not installed",
|
|
462
|
+
available_plugins: "Available plugins",
|
|
463
|
+
available_skills: "Available Skills",
|
|
464
|
+
skill_total_count: "Total {count} Skills",
|
|
465
|
+
wizard_confirm_install_format: "About to install {count} plugins to {tool} ({level} level):",
|
|
466
|
+
wizard_install_path: "Install path",
|
|
467
|
+
update_install_hint: "Use {cmd} to install plugins",
|
|
468
|
+
hint_select_move: "Navigate",
|
|
469
|
+
hint_select_space: "Space",
|
|
470
|
+
hint_select_check: "Toggle",
|
|
471
|
+
hint_select_confirm: "Confirm",
|
|
472
|
+
hint_checkbox_suffix: "Press enter without selection to go back",
|
|
473
|
+
doctor_not_installed: "Not installed",
|
|
474
|
+
doctor_links_valid: "{count} symlinks all valid",
|
|
475
|
+
doctor_broken_links: "{count} broken links",
|
|
476
|
+
doctor_fixed: "Cleaned {count} broken links",
|
|
477
|
+
doctor_not_exist: "Does not exist",
|
|
478
|
+
doctor_created: "Created {path}",
|
|
479
|
+
doctor_config_exists: "Exists",
|
|
480
|
+
doctor_config_not_exist: "Does not exist",
|
|
481
|
+
doctor_result_with_fix: "{warnings} warnings, {errors} errors, {fixes} fixed",
|
|
482
|
+
doctor_result_without_fix: "{warnings} warnings, {errors} errors",
|
|
483
|
+
count_suffix: "",
|
|
484
|
+
try_prompt: "help me develop an Abs operator"
|
|
423
485
|
};
|
|
424
486
|
var currentLang = "zh_CN";
|
|
425
487
|
function setLanguage(lang) {
|
|
@@ -682,6 +744,9 @@ function scanInstalled() {
|
|
|
682
744
|
for (const manifest of manifests) {
|
|
683
745
|
const plugin = plugins.find((p) => p.id === manifest.team);
|
|
684
746
|
if (!plugin) continue;
|
|
747
|
+
if (!verifyPluginFilesExist(configRoot, manifest, tool, level)) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
685
750
|
const alreadyAdded = installed.some(
|
|
686
751
|
(p) => p.id === plugin.id && p.tool === tool && p.level === level
|
|
687
752
|
);
|
|
@@ -701,6 +766,32 @@ function scanInstalled() {
|
|
|
701
766
|
}
|
|
702
767
|
return installed;
|
|
703
768
|
}
|
|
769
|
+
function verifyPluginFilesExist(configRoot, manifest, tool, level) {
|
|
770
|
+
const skillsDir = join3(configRoot, "skills");
|
|
771
|
+
const agentsDir = join3(configRoot, "agents");
|
|
772
|
+
if (manifest.installed_skills && manifest.installed_skills.length > 0) {
|
|
773
|
+
const hasAnySkill = manifest.installed_skills.some((skillId) => {
|
|
774
|
+
const skillPath = join3(skillsDir, skillId);
|
|
775
|
+
return existsSync3(skillPath);
|
|
776
|
+
});
|
|
777
|
+
if (!hasAnySkill) return false;
|
|
778
|
+
}
|
|
779
|
+
if (manifest.installed_agents && manifest.installed_agents.length > 0) {
|
|
780
|
+
const hasAnyAgent = manifest.installed_agents.some((agentId) => {
|
|
781
|
+
const agentPath = join3(agentsDir, agentId);
|
|
782
|
+
return existsSync3(agentPath);
|
|
783
|
+
});
|
|
784
|
+
if (!hasAnyAgent) return false;
|
|
785
|
+
}
|
|
786
|
+
let agentsMdPath;
|
|
787
|
+
if (tool === "opencode" && level === "project") {
|
|
788
|
+
agentsMdPath = join3(configRoot, "..", "AGENTS.md");
|
|
789
|
+
} else {
|
|
790
|
+
agentsMdPath = join3(configRoot, "AGENTS.md");
|
|
791
|
+
}
|
|
792
|
+
if (!existsSync3(agentsMdPath)) return false;
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
704
795
|
|
|
705
796
|
// src/utils/logger.ts
|
|
706
797
|
import chalk from "chalk";
|
|
@@ -756,10 +847,10 @@ function printBoxTitle(title, width = 65) {
|
|
|
756
847
|
}
|
|
757
848
|
function showOperationHints(isCheckbox = false) {
|
|
758
849
|
if (isCheckbox) {
|
|
759
|
-
console.log(` \u{1F4A1} \x1B[36m\u2191\u2193\x1B[0m
|
|
850
|
+
console.log(` \u{1F4A1} \x1B[36m\u2191\u2193\x1B[0m ${t("hint_select_move")} | \x1B[36m${t("hint_select_space")}\x1B[0m ${t("hint_select_check")} | \x1B[36m\u23CE\x1B[0m ${t("hint_select_confirm")}\uFF08${t("hint_checkbox_suffix")}\uFF09
|
|
760
851
|
`);
|
|
761
852
|
} else {
|
|
762
|
-
console.log(` \u{1F4A1} \x1B[36m\u2191\u2193\x1B[0m
|
|
853
|
+
console.log(` \u{1F4A1} \x1B[36m\u2191\u2193\x1B[0m ${t("hint_select_move")} | \x1B[36m\u23CE\x1B[0m ${t("hint_select_confirm")}
|
|
763
854
|
`);
|
|
764
855
|
}
|
|
765
856
|
}
|
|
@@ -1068,8 +1159,8 @@ async function stepConfirm(tool, level, plugins) {
|
|
|
1068
1159
|
const displayPath = getConfigRoot(tool, level);
|
|
1069
1160
|
const toolName = getToolDisplayName(tool);
|
|
1070
1161
|
const levelText = level === "project" ? "project" : "global";
|
|
1071
|
-
logger.info(
|
|
1072
|
-
logger.info(
|
|
1162
|
+
logger.info(t("wizard_confirm_install_format").replace("{count}", chalk2.bold(String(plugins.length))).replace("{tool}", chalk2.green(toolName)).replace("{level}", chalk2.cyan(levelText)));
|
|
1163
|
+
logger.info(`${t("wizard_install_path")}: ${chalk2.cyan(displayPath)}`);
|
|
1073
1164
|
for (const plugin of selectedPlugins) {
|
|
1074
1165
|
if (plugin) {
|
|
1075
1166
|
logger.step(` \u2022 ${chalk2.bold(plugin.displayName)}`);
|
|
@@ -1096,7 +1187,7 @@ import { execa as execa2 } from "execa";
|
|
|
1096
1187
|
|
|
1097
1188
|
// src/core/scanner.ts
|
|
1098
1189
|
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1099
|
-
import { join as join5 } from "path";
|
|
1190
|
+
import { join as join5, dirname } from "path";
|
|
1100
1191
|
import { parse as parseYaml } from "yaml";
|
|
1101
1192
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1102
1193
|
"node_modules",
|
|
@@ -1779,7 +1870,13 @@ function removeSkillsFromRecord(skillIds, tool, level, installPath) {
|
|
|
1779
1870
|
}
|
|
1780
1871
|
function getInstalledSkills(tool, level, installPath) {
|
|
1781
1872
|
const record = readSkillRecord();
|
|
1782
|
-
|
|
1873
|
+
const recordedSkills = record[tool]?.[level]?.[installPath]?.skills || [];
|
|
1874
|
+
const configRoot = getConfigRoot(tool, level);
|
|
1875
|
+
const skillsDir = join7(configRoot, "skills");
|
|
1876
|
+
return recordedSkills.filter((skillId) => {
|
|
1877
|
+
const skillPath = join7(skillsDir, skillId);
|
|
1878
|
+
return existsSync7(skillPath) || isSymlink(skillPath);
|
|
1879
|
+
});
|
|
1783
1880
|
}
|
|
1784
1881
|
|
|
1785
1882
|
// src/core/backup.ts
|
|
@@ -2135,10 +2232,10 @@ async function uninstallSkills(skillIds, tool, level) {
|
|
|
2135
2232
|
unlinkSync3(targetPath);
|
|
2136
2233
|
results.push({ skillId, success: true });
|
|
2137
2234
|
removedIds.push(skillId);
|
|
2138
|
-
logger.step(`
|
|
2235
|
+
logger.step(` ${t("uninstall_remove_label")}: ${skillId}`);
|
|
2139
2236
|
} else {
|
|
2140
2237
|
results.push({ skillId, success: false, error: "Not installed" });
|
|
2141
|
-
logger.warn(`${skillId}
|
|
2238
|
+
logger.warn(`${skillId} ${t("uninstall_skill_not_installed")}`);
|
|
2142
2239
|
}
|
|
2143
2240
|
} catch (error) {
|
|
2144
2241
|
results.push({ skillId, success: false, error: error instanceof Error ? error.message : "Unknown error" });
|
|
@@ -2152,14 +2249,14 @@ async function uninstallSkills(skillIds, tool, level) {
|
|
|
2152
2249
|
const entries = readdirSync3(skillsDir);
|
|
2153
2250
|
if (entries.length === 0) {
|
|
2154
2251
|
rmdirSync(skillsDir);
|
|
2155
|
-
logger.step(`
|
|
2252
|
+
logger.step(` ${t("uninstall_clean_empty_dir")}: skills/`);
|
|
2156
2253
|
}
|
|
2157
2254
|
} catch {
|
|
2158
2255
|
}
|
|
2159
2256
|
}
|
|
2160
2257
|
return results;
|
|
2161
2258
|
}
|
|
2162
|
-
async function interactiveSkillSelect() {
|
|
2259
|
+
async function interactiveSkillSelect(tool, level) {
|
|
2163
2260
|
const BACK3 = "__back__";
|
|
2164
2261
|
const CANCEL3 = "__cancel__";
|
|
2165
2262
|
const categories = getAllCategories();
|
|
@@ -2195,15 +2292,8 @@ async function interactiveSkillSelect() {
|
|
|
2195
2292
|
const category = categories.find((c) => c.id === selectedCategoryId);
|
|
2196
2293
|
if (!category) return [];
|
|
2197
2294
|
printBoxTitle(t("wizard_step_4_skill_title").replace("{category}", category.name));
|
|
2198
|
-
const
|
|
2199
|
-
const
|
|
2200
|
-
const installedSkills = [];
|
|
2201
|
-
for (const tk of tools) {
|
|
2202
|
-
for (const l of levels) {
|
|
2203
|
-
const path = l === "project" ? process.cwd() : getConfigRoot(tk, l);
|
|
2204
|
-
installedSkills.push(...getInstalledSkills(tk, l, path));
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2295
|
+
const installPath = level === "project" ? process.cwd() : getConfigRoot(tool, level);
|
|
2296
|
+
const installedSkills = getInstalledSkills(tool, level, installPath);
|
|
2207
2297
|
const installedSet = new Set(installedSkills);
|
|
2208
2298
|
const skillChoices = category.skills.map((skill) => {
|
|
2209
2299
|
const isInstalled = installedSet.has(skill.id);
|
|
@@ -2268,7 +2358,7 @@ function listAllSkills() {
|
|
|
2268
2358
|
console.log(table.toString());
|
|
2269
2359
|
}
|
|
2270
2360
|
console.log();
|
|
2271
|
-
console.log(chalk4.dim(`
|
|
2361
|
+
console.log(chalk4.dim(` ${t("skill_total_count").replace("{count}", String(getAllSkills().length))}`));
|
|
2272
2362
|
console.log();
|
|
2273
2363
|
}
|
|
2274
2364
|
function isSymlink2(path) {
|
|
@@ -2305,7 +2395,8 @@ function printPluginList(plugins, installed) {
|
|
|
2305
2395
|
]);
|
|
2306
2396
|
});
|
|
2307
2397
|
console.log();
|
|
2308
|
-
|
|
2398
|
+
const countSuffix = t("count_suffix");
|
|
2399
|
+
console.log(chalk5.bold(` ${t("list_title")} (${plugins.length}${countSuffix ? " " + countSuffix : ""})`));
|
|
2309
2400
|
console.log(table.toString());
|
|
2310
2401
|
console.log();
|
|
2311
2402
|
}
|
|
@@ -2338,15 +2429,16 @@ function printEnhancedSummary(results, tool, configRoot) {
|
|
|
2338
2429
|
return;
|
|
2339
2430
|
}
|
|
2340
2431
|
const toolName = getToolDisplayName(tool);
|
|
2432
|
+
const countSuffix = t("count_suffix");
|
|
2341
2433
|
const lines = [
|
|
2342
2434
|
chalk5.bold(` ${t("install_done")}!`),
|
|
2343
2435
|
"",
|
|
2344
2436
|
` ${chalk5.dim(t("install_installed_to") + ":")} ${configRoot}`,
|
|
2345
|
-
` ${chalk5.dim(t("install_skills_count") + ":")} ${totalSkills}
|
|
2437
|
+
` ${chalk5.dim(t("install_skills_count") + ":")} ${totalSkills}${countSuffix ? " " + countSuffix : ""} ${chalk5.dim("|")} ${chalk5.dim(t("install_agents_count") + ":")} ${totalAgents}${countSuffix ? " " + countSuffix : ""}`,
|
|
2346
2438
|
"",
|
|
2347
2439
|
chalk5.bold(` ${t("install_enhanced_next_steps")}:`),
|
|
2348
2440
|
` ${chalk5.cyan("1.")} ${t("install_enhanced_launch")}: ${chalk5.green(toolName.toLowerCase())}`,
|
|
2349
|
-
` ${chalk5.cyan("2.")} ${t("install_enhanced_try")}: ${chalk5.green("
|
|
2441
|
+
` ${chalk5.cyan("2.")} ${t("install_enhanced_try")}: ${chalk5.green(t("try_prompt"))}`,
|
|
2350
2442
|
` ${chalk5.cyan("3.")} ${t("install_enhanced_more")}: ${chalk5.green("install-helper list")}`,
|
|
2351
2443
|
` ${chalk5.cyan("4.")} ${t("install_enhanced_check")}: ${chalk5.green("install-helper doctor")}`,
|
|
2352
2444
|
""
|
|
@@ -2536,7 +2628,7 @@ async function skillInstallFlow() {
|
|
|
2536
2628
|
break;
|
|
2537
2629
|
}
|
|
2538
2630
|
case 2: {
|
|
2539
|
-
const selectedSkills = await interactiveSkillSelect();
|
|
2631
|
+
const selectedSkills = await interactiveSkillSelect(tool, level);
|
|
2540
2632
|
if (selectedSkills === "back") {
|
|
2541
2633
|
step = 1;
|
|
2542
2634
|
break;
|
|
@@ -2554,7 +2646,7 @@ async function skillInstallFlow() {
|
|
|
2554
2646
|
spinner.start();
|
|
2555
2647
|
const repoPath = await repoManager.ensureRepo();
|
|
2556
2648
|
spinner.succeed(t("loading_plugin_data_complete"));
|
|
2557
|
-
logger.info(`${t("skill_install_progress")} ${chalk6.bold(selectedSkills.length)}
|
|
2649
|
+
logger.info(`${t("skill_install_progress")} ${t("skill_count_format").replace("{count}", chalk6.bold(String(selectedSkills.length)))}...`);
|
|
2558
2650
|
const results = await installSkills(selectedSkills, tool, level, repoPath);
|
|
2559
2651
|
let successCount = 0;
|
|
2560
2652
|
let failCount = 0;
|
|
@@ -2572,7 +2664,7 @@ async function skillInstallFlow() {
|
|
|
2572
2664
|
}
|
|
2573
2665
|
const configRoot = getConfigRoot(tool, level);
|
|
2574
2666
|
logger.blank();
|
|
2575
|
-
logger.success(`${t("skill_install_done")}: ${chalk6.green(
|
|
2667
|
+
logger.success(`${t("skill_install_done")}: ${chalk6.green(t("result_success_format").replace("{count}", String(successCount)))}, ${failCount > 0 ? chalk6.red(t("result_failed_format").replace("{count}", String(failCount))) : chalk6.dim(t("result_failed_format").replace("{count}", String(failCount)))}`);
|
|
2576
2668
|
logger.blank();
|
|
2577
2669
|
logger.info(`${t("install_to")}: ${chalk6.cyan(configRoot + "/skills/")}`);
|
|
2578
2670
|
logger.info(`${t("start_to_use").replace("{tool}", chalk6.green(tool))}`);
|
|
@@ -2618,7 +2710,7 @@ async function doctorCommand(options = {}) {
|
|
|
2618
2710
|
chalk7.green(" \u2713") + ` ${getToolDisplayName(tool)}${detected.version ? ` v${detected.version}` : ""}`
|
|
2619
2711
|
);
|
|
2620
2712
|
} else {
|
|
2621
|
-
console.log(chalk7.dim(" \u2014") + ` ${getToolDisplayName(tool)} \u2014
|
|
2713
|
+
console.log(chalk7.dim(" \u2014") + ` ${getToolDisplayName(tool)} \u2014 ${t("doctor_not_installed")}`);
|
|
2622
2714
|
}
|
|
2623
2715
|
}
|
|
2624
2716
|
console.log();
|
|
@@ -2633,7 +2725,7 @@ async function doctorCommand(options = {}) {
|
|
|
2633
2725
|
chalk7.green(" \u2713") + ` ${plugin.id.padEnd(30)} ${inst.skillsCount} skills, ${inst.agentsCount} agents`
|
|
2634
2726
|
);
|
|
2635
2727
|
} else {
|
|
2636
|
-
console.log(chalk7.dim(" \u2014") + ` ${plugin.id.padEnd(30)} \u2014
|
|
2728
|
+
console.log(chalk7.dim(" \u2014") + ` ${plugin.id.padEnd(30)} \u2014 ${t("doctor_not_installed")}`);
|
|
2637
2729
|
}
|
|
2638
2730
|
}
|
|
2639
2731
|
console.log();
|
|
@@ -2646,44 +2738,44 @@ async function doctorCommand(options = {}) {
|
|
|
2646
2738
|
const brokenLinks = checkBrokenLinks(skillsDir);
|
|
2647
2739
|
if (brokenLinks === 0) {
|
|
2648
2740
|
const count = readdirSync4(skillsDir).length;
|
|
2649
|
-
console.log(chalk7.green(" \u2713") + ` ${skillsDir} \u2014 ${count}
|
|
2741
|
+
console.log(chalk7.green(" \u2713") + ` ${skillsDir} \u2014 ${t("doctor_links_valid").replace("{count}", String(count))}`);
|
|
2650
2742
|
} else {
|
|
2651
|
-
console.log(chalk7.yellow(" \u26A0") + ` ${skillsDir} \u2014 ${brokenLinks}
|
|
2743
|
+
console.log(chalk7.yellow(" \u26A0") + ` ${skillsDir} \u2014 ${t("doctor_broken_links").replace("{count}", String(brokenLinks))}`);
|
|
2652
2744
|
warnings++;
|
|
2653
2745
|
if (options.fix) {
|
|
2654
2746
|
const fixed = fixBrokenLinks(skillsDir);
|
|
2655
2747
|
fixes += fixed;
|
|
2656
|
-
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_cleaning")}:
|
|
2748
|
+
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_cleaning")}: ${t("doctor_fixed").replace("{count}", String(fixed))}`);
|
|
2657
2749
|
}
|
|
2658
2750
|
}
|
|
2659
2751
|
} else {
|
|
2660
|
-
console.log(chalk7.dim(" \u2014") + ` ${skillsDir} \u2014
|
|
2752
|
+
console.log(chalk7.dim(" \u2014") + ` ${skillsDir} \u2014 ${t("doctor_not_exist")}`);
|
|
2661
2753
|
if (options.fix) {
|
|
2662
2754
|
mkdirSync6(skillsDir, { recursive: true });
|
|
2663
2755
|
fixes++;
|
|
2664
|
-
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_rebuilding")}:
|
|
2756
|
+
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_rebuilding")}: ${t("doctor_created").replace("{path}", skillsDir)}`);
|
|
2665
2757
|
}
|
|
2666
2758
|
}
|
|
2667
2759
|
if (existsSync12(agentsDir)) {
|
|
2668
2760
|
const brokenLinks = checkBrokenLinks(agentsDir);
|
|
2669
2761
|
if (brokenLinks === 0) {
|
|
2670
2762
|
const count = readdirSync4(agentsDir).length;
|
|
2671
|
-
console.log(chalk7.green(" \u2713") + ` ${agentsDir} \u2014 ${count}
|
|
2763
|
+
console.log(chalk7.green(" \u2713") + ` ${agentsDir} \u2014 ${t("doctor_links_valid").replace("{count}", String(count))}`);
|
|
2672
2764
|
} else {
|
|
2673
|
-
console.log(chalk7.yellow(" \u26A0") + ` ${agentsDir} \u2014 ${brokenLinks}
|
|
2765
|
+
console.log(chalk7.yellow(" \u26A0") + ` ${agentsDir} \u2014 ${t("doctor_broken_links").replace("{count}", String(brokenLinks))}`);
|
|
2674
2766
|
warnings++;
|
|
2675
2767
|
if (options.fix) {
|
|
2676
2768
|
const fixed = fixBrokenLinks(agentsDir);
|
|
2677
2769
|
fixes += fixed;
|
|
2678
|
-
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_cleaning")}:
|
|
2770
|
+
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_cleaning")}: ${t("doctor_fixed").replace("{count}", String(fixed))}`);
|
|
2679
2771
|
}
|
|
2680
2772
|
}
|
|
2681
2773
|
} else {
|
|
2682
|
-
console.log(chalk7.dim(" \u2014") + ` ${agentsDir} \u2014
|
|
2774
|
+
console.log(chalk7.dim(" \u2014") + ` ${agentsDir} \u2014 ${t("doctor_not_exist")}`);
|
|
2683
2775
|
if (options.fix) {
|
|
2684
2776
|
mkdirSync6(agentsDir, { recursive: true });
|
|
2685
2777
|
fixes++;
|
|
2686
|
-
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_rebuilding")}:
|
|
2778
|
+
console.log(chalk7.green(" \u2713") + ` ${t("doctor_fix_rebuilding")}: ${t("doctor_created").replace("{path}", agentsDir)}`);
|
|
2687
2779
|
}
|
|
2688
2780
|
}
|
|
2689
2781
|
console.log();
|
|
@@ -2691,19 +2783,19 @@ async function doctorCommand(options = {}) {
|
|
|
2691
2783
|
const configFile = getConfigFileName(primaryTool);
|
|
2692
2784
|
const configPath = join12(process.cwd(), configFile);
|
|
2693
2785
|
if (existsSync12(configPath)) {
|
|
2694
|
-
console.log(chalk7.green(" \u2713") + ` ${configFile}
|
|
2786
|
+
console.log(chalk7.green(" \u2713") + ` ${configFile} ${t("doctor_config_exists")}`);
|
|
2695
2787
|
} else {
|
|
2696
|
-
console.log(chalk7.dim(" \u2014") + ` ${configFile}
|
|
2788
|
+
console.log(chalk7.dim(" \u2014") + ` ${configFile} ${t("doctor_config_not_exist")}`);
|
|
2697
2789
|
}
|
|
2698
2790
|
console.log();
|
|
2699
2791
|
console.log(chalk7.dim(" " + "\u2500".repeat(46)));
|
|
2700
2792
|
if (options.fix && fixes > 0) {
|
|
2701
2793
|
console.log(
|
|
2702
|
-
` ${t("doctor_result")}: ${warnings}
|
|
2794
|
+
` ${t("doctor_result")}: ${t("doctor_result_with_fix").replace("{warnings}", String(warnings)).replace("{errors}", String(errors)).replace("{fixes}", String(fixes))}`
|
|
2703
2795
|
);
|
|
2704
2796
|
} else {
|
|
2705
2797
|
console.log(
|
|
2706
|
-
` ${t("doctor_result")}: ${warnings}
|
|
2798
|
+
` ${t("doctor_result")}: ${t("doctor_result_without_fix").replace("{warnings}", String(warnings)).replace("{errors}", String(errors))}`
|
|
2707
2799
|
);
|
|
2708
2800
|
}
|
|
2709
2801
|
console.log();
|
|
@@ -2798,9 +2890,9 @@ async function installCommand(names, options) {
|
|
|
2798
2890
|
try {
|
|
2799
2891
|
const scanRepoManager = createRepositoryManager();
|
|
2800
2892
|
await scanRepoManager.ensureRepoAndScan();
|
|
2801
|
-
scanSpinner2.succeed("
|
|
2893
|
+
scanSpinner2.succeed(t("loading_skills_list_complete"));
|
|
2802
2894
|
} catch {
|
|
2803
|
-
scanSpinner2.warn("
|
|
2895
|
+
scanSpinner2.warn(t("loading_skills_list_failed"));
|
|
2804
2896
|
}
|
|
2805
2897
|
let tool2;
|
|
2806
2898
|
if (options.tool) {
|
|
@@ -2810,11 +2902,12 @@ async function installCommand(names, options) {
|
|
|
2810
2902
|
if (detected === "back" || detected === "cancel") return;
|
|
2811
2903
|
tool2 = detected;
|
|
2812
2904
|
}
|
|
2905
|
+
const level2 = options.level ? validateLevel(options.level) : "project";
|
|
2813
2906
|
let step = 1;
|
|
2814
2907
|
while (true) {
|
|
2815
2908
|
switch (step) {
|
|
2816
2909
|
case 1: {
|
|
2817
|
-
const selectedSkills = await interactiveSkillSelect();
|
|
2910
|
+
const selectedSkills = await interactiveSkillSelect(tool2, level2);
|
|
2818
2911
|
if (selectedSkills === "back") {
|
|
2819
2912
|
const redetected = await selectToolWithDetection();
|
|
2820
2913
|
if (redetected === "back" || redetected === "cancel") return;
|
|
@@ -2831,9 +2924,8 @@ async function installCommand(names, options) {
|
|
|
2831
2924
|
const spinner = createSpinner(t("loading_plugin_data"));
|
|
2832
2925
|
spinner.start();
|
|
2833
2926
|
const repoPath = await repoManager.ensureRepo();
|
|
2834
|
-
spinner.succeed("
|
|
2835
|
-
logger.info(`${t("skill_install_progress")} ${chalk9.bold(names.length)}
|
|
2836
|
-
const level2 = options.level ? validateLevel(options.level) : "project";
|
|
2927
|
+
spinner.succeed(t("loading_plugin_data_complete"));
|
|
2928
|
+
logger.info(`${t("skill_install_progress")} ${t("skill_count_format").replace("{count}", chalk9.bold(String(names.length)))}...`);
|
|
2837
2929
|
const results = await installSkills(names, tool2, level2, repoPath);
|
|
2838
2930
|
let successCount = 0;
|
|
2839
2931
|
let failCount = 0;
|
|
@@ -2851,7 +2943,7 @@ async function installCommand(names, options) {
|
|
|
2851
2943
|
}
|
|
2852
2944
|
const configRoot = getConfigRoot(tool2, level2);
|
|
2853
2945
|
logger.blank();
|
|
2854
|
-
logger.success(`${t("skill_install_done")}: ${chalk9.green(
|
|
2946
|
+
logger.success(`${t("skill_install_done")}: ${chalk9.green(t("result_success_format").replace("{count}", String(successCount)))}, ${failCount > 0 ? chalk9.red(t("result_failed_format").replace("{count}", String(failCount))) : chalk9.dim(t("result_failed_format").replace("{count}", String(failCount)))}`);
|
|
2855
2947
|
logger.blank();
|
|
2856
2948
|
logger.info(`${t("install_to")}: ${chalk9.cyan(configRoot + "/skills/")}`);
|
|
2857
2949
|
logger.info(`${t("start_to_use").replace("{tool}", chalk9.green(tool2))}`);
|
|
@@ -2866,9 +2958,9 @@ async function installCommand(names, options) {
|
|
|
2866
2958
|
try {
|
|
2867
2959
|
const scanRepoManager = createRepositoryManager();
|
|
2868
2960
|
await scanRepoManager.ensureRepoAndScan();
|
|
2869
|
-
scanSpinner.succeed("
|
|
2961
|
+
scanSpinner.succeed(t("loading_skills_list_complete"));
|
|
2870
2962
|
} catch {
|
|
2871
|
-
scanSpinner.warn("
|
|
2963
|
+
scanSpinner.warn(t("loading_skills_list_failed"));
|
|
2872
2964
|
}
|
|
2873
2965
|
const plugins = [];
|
|
2874
2966
|
const skills = [];
|
|
@@ -2885,11 +2977,11 @@ async function installCommand(names, options) {
|
|
|
2885
2977
|
}
|
|
2886
2978
|
logger.error(`${t("error_plugin_not_found")}: ${name}`);
|
|
2887
2979
|
const allPlugins = getAllPlugins();
|
|
2888
|
-
logger.info("
|
|
2980
|
+
logger.info(t("available_plugins") + ":");
|
|
2889
2981
|
for (const p of allPlugins) {
|
|
2890
2982
|
logger.step(` ${p.id} (${p.aliases.join(", ")})`);
|
|
2891
2983
|
}
|
|
2892
|
-
logger.info(
|
|
2984
|
+
logger.info(`${t("available_skills")}: ${chalk9.cyan("install-helper install --list")}`);
|
|
2893
2985
|
return;
|
|
2894
2986
|
}
|
|
2895
2987
|
let tool;
|
|
@@ -2961,7 +3053,7 @@ async function installCommand(names, options) {
|
|
|
2961
3053
|
spinner.start();
|
|
2962
3054
|
const repoPath = await repoManager.ensureRepo();
|
|
2963
3055
|
spinner.succeed(t("loading_plugin_data_complete"));
|
|
2964
|
-
logger.info(`${t("skill_install_progress")} ${chalk9.bold(skills.length)}
|
|
3056
|
+
logger.info(`${t("skill_install_progress")} ${t("skill_count_format").replace("{count}", chalk9.bold(String(skills.length)))}...`);
|
|
2965
3057
|
const results = await installSkills(skills, tool, level, repoPath);
|
|
2966
3058
|
let successCount = 0;
|
|
2967
3059
|
let failCount = 0;
|
|
@@ -2978,7 +3070,7 @@ async function installCommand(names, options) {
|
|
|
2978
3070
|
}
|
|
2979
3071
|
}
|
|
2980
3072
|
logger.blank();
|
|
2981
|
-
logger.success(`${t("skill_install_done")}: ${chalk9.green(
|
|
3073
|
+
logger.success(`${t("skill_install_done")}: ${chalk9.green(t("result_success_format").replace("{count}", String(successCount)))}, ${failCount > 0 ? chalk9.red(t("result_failed_format").replace("{count}", String(failCount))) : chalk9.dim(t("result_failed_format").replace("{count}", String(failCount)))}`);
|
|
2982
3074
|
logger.blank();
|
|
2983
3075
|
logger.info(`${t("install_to")}: ${chalk9.cyan(getConfigRoot(tool, level) + "/skills/")}`);
|
|
2984
3076
|
logger.info(`${t("start_to_use").replace("{tool}", chalk9.green(tool))}`);
|
|
@@ -3050,7 +3142,7 @@ async function uninstallCommand(names, options) {
|
|
|
3050
3142
|
if (existsSync13(filePath) || isSymlink3(filePath)) {
|
|
3051
3143
|
unlinkSync5(filePath);
|
|
3052
3144
|
const name = filePath.split("/").pop() || filePath;
|
|
3053
|
-
logger.step(`
|
|
3145
|
+
logger.step(` ${t("uninstall_remove_label")}: ${name}`);
|
|
3054
3146
|
removedFiles++;
|
|
3055
3147
|
}
|
|
3056
3148
|
} catch {
|
|
@@ -3068,7 +3160,7 @@ async function uninstallCommand(names, options) {
|
|
|
3068
3160
|
if (entries.length === 0) {
|
|
3069
3161
|
rmdirSync2(dirPath);
|
|
3070
3162
|
const name = dirPath.split("/").pop() || dirPath;
|
|
3071
|
-
logger.step(`
|
|
3163
|
+
logger.step(` ${t("uninstall_clean_empty_dir")}: ${name}/`);
|
|
3072
3164
|
removedDirs++;
|
|
3073
3165
|
}
|
|
3074
3166
|
}
|
|
@@ -3096,13 +3188,13 @@ async function uninstallCommand(names, options) {
|
|
|
3096
3188
|
}
|
|
3097
3189
|
const totalRemoved = removedFiles + removedDirs;
|
|
3098
3190
|
if (totalRemoved > 0) {
|
|
3099
|
-
logger.success(`${t("uninstall_removed")} ${plugin.displayName}\uFF08
|
|
3191
|
+
logger.success(`${t("uninstall_removed")} ${plugin.displayName}\uFF08${t("uninstall_remove_file_format").replace("{count}", String(removedFiles))}\uFF0C${t("uninstall_remove_dir_format").replace("{count}", String(removedDirs))}\uFF09`);
|
|
3100
3192
|
} else {
|
|
3101
3193
|
logger.warn(`${plugin.displayName}${t("uninstall_files_gone")}`);
|
|
3102
3194
|
}
|
|
3103
3195
|
}
|
|
3104
3196
|
if (skills.length > 0) {
|
|
3105
|
-
logger.info(`${t("uninstall_skills_in_progress")} ${skills.length}
|
|
3197
|
+
logger.info(`${t("uninstall_skills_in_progress")} ${t("skill_count_format").replace("{count}", String(skills.length))}...`);
|
|
3106
3198
|
const results = await uninstallSkills(skills, tool, level);
|
|
3107
3199
|
let successCount = 0;
|
|
3108
3200
|
let failCount = 0;
|
|
@@ -3114,7 +3206,7 @@ async function uninstallCommand(names, options) {
|
|
|
3114
3206
|
}
|
|
3115
3207
|
}
|
|
3116
3208
|
logger.blank();
|
|
3117
|
-
logger.success(`${t("skill_uninstall_done")}: ${chalk10.green(
|
|
3209
|
+
logger.success(`${t("skill_uninstall_done")}: ${chalk10.green(t("result_success_format").replace("{count}", String(successCount)))}, ${failCount > 0 ? chalk10.red(t("result_failed_format").replace("{count}", String(failCount))) : chalk10.dim(t("result_failed_format").replace("{count}", String(failCount)))}`);
|
|
3118
3210
|
logger.blank();
|
|
3119
3211
|
}
|
|
3120
3212
|
}
|
|
@@ -3145,7 +3237,7 @@ async function updateCommand(pluginNames, options) {
|
|
|
3145
3237
|
pluginsToUpdate = manifests.map((m) => m.team);
|
|
3146
3238
|
if (pluginsToUpdate.length === 0) {
|
|
3147
3239
|
logger.info(t("update_no_plugins"));
|
|
3148
|
-
logger.info(
|
|
3240
|
+
logger.info(t("update_install_hint").replace("{cmd}", chalk11.cyan("install-helper install <plugin>")));
|
|
3149
3241
|
return;
|
|
3150
3242
|
}
|
|
3151
3243
|
} else {
|
|
@@ -3296,7 +3388,7 @@ function createCLI() {
|
|
|
3296
3388
|
setLanguage(config.language);
|
|
3297
3389
|
}
|
|
3298
3390
|
const program2 = new Command();
|
|
3299
|
-
program2.name("install-helper").description("CANNBot Install Helper - Interactive installer for CANN operator development skills").version("0.0.2-beta.
|
|
3391
|
+
program2.name("install-helper").description("CANNBot Install Helper - Interactive installer for CANN operator development skills").version("0.0.2-beta.15");
|
|
3300
3392
|
program2.command("init", { isDefault: false }).description("Run interactive installation wizard").action(async () => {
|
|
3301
3393
|
await initCommand();
|
|
3302
3394
|
});
|
|
@@ -3333,7 +3425,7 @@ function createCLI() {
|
|
|
3333
3425
|
// src/index.ts
|
|
3334
3426
|
process.on("uncaughtException", (error) => {
|
|
3335
3427
|
if (error.name === "ExitPromptError") {
|
|
3336
|
-
console.log("\n
|
|
3428
|
+
console.log("\n" + t("init_cancelled"));
|
|
3337
3429
|
process.exit(0);
|
|
3338
3430
|
}
|
|
3339
3431
|
throw error;
|