coderev-cli 1.0.24 → 1.0.26
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 +248 -7
- package/package.json +2 -2
- package/src/bin.js +3 -0
- package/src/cli.js +27 -0
- package/src/doctor.js +573 -0
package/README.md
CHANGED
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
- [coderev config(配置管理)](#coderev-config配置管理)
|
|
28
28
|
- [coderev init(初始化)](#coderev-init初始化)
|
|
29
29
|
- [coderev serve(GitHub App 自动审查)](#coderev-servegithub-app-自动审查)
|
|
30
|
+
- [coderev models(模型模板)🆕](#coderev-models模型模板)
|
|
31
|
+
- [coderev setup(模型配置)🆕](#coderev-setup模型配置)
|
|
32
|
+
- [coderev rules(规则市场)🆕](#coderev-rules规则市场)
|
|
33
|
+
- [coderev doctor(环境诊断)🆕](#coderev-doctor环境诊断)
|
|
30
34
|
- [配置详解](#配置详解)
|
|
31
35
|
- [平台集成](#平台集成)
|
|
32
36
|
- [CI/CD 集成](#cicd-集成)
|
|
37
|
+
- [VS Code 扩展 🆕](#vs-code-扩展)
|
|
33
38
|
- [FAQ / 常见问题](#faq--常见问题)
|
|
34
39
|
|
|
35
40
|
---
|
|
@@ -94,19 +99,25 @@ coderev init
|
|
|
94
99
|
|
|
95
100
|
会在当前目录创建 `.coderevrc.json` 配置文件。
|
|
96
101
|
|
|
97
|
-
### 第 2
|
|
102
|
+
### 第 2 步:设置模型和 API Key
|
|
98
103
|
|
|
99
|
-
coderev
|
|
104
|
+
coderev 内置了 11 个热门模型模板,只需选择模板 + 设置 API Key 即可:
|
|
100
105
|
|
|
101
106
|
```bash
|
|
102
|
-
#
|
|
103
|
-
|
|
107
|
+
# 查看所有可用模型
|
|
108
|
+
coderev models
|
|
104
109
|
|
|
105
|
-
#
|
|
106
|
-
|
|
110
|
+
# 选择 DeepSeek(推荐,性价比最高)
|
|
111
|
+
coderev setup --model deepseek
|
|
112
|
+
|
|
113
|
+
# 设置 API Key
|
|
114
|
+
# Windows PowerShell:
|
|
115
|
+
$env:DEEPSEEK_API_KEY="***"
|
|
116
|
+
# Linux / macOS:
|
|
117
|
+
export DEEPSEEK_API_KEY="***"
|
|
107
118
|
```
|
|
108
119
|
|
|
109
|
-
|
|
120
|
+
支持所有主流 AI 提供商(见 [coderev models](#coderev-models模型模板) 章节)。
|
|
110
121
|
|
|
111
122
|
### 第 3 步:运行审查
|
|
112
123
|
|
|
@@ -481,6 +492,187 @@ GITHUB_APP_ID=123456 GITHUB_APP_WEBHOOK_SECRET=mysecret coderev serve
|
|
|
481
492
|
|
|
482
493
|
---
|
|
483
494
|
|
|
495
|
+
### coderev models(模型模板)🆕 v1.0.24
|
|
496
|
+
|
|
497
|
+
**作用**:列出所有内置的 AI 模型模板及其配置。只需选择一个模板并设置对应的 API Key 即可使用。
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
coderev models
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**内置模板清单**:
|
|
504
|
+
|
|
505
|
+
| 模板 | 提供商 | 默认模型 | API Key 环境变量 | 说明 |
|
|
506
|
+
|------|--------|---------|-----------------|------|
|
|
507
|
+
| ⭐ `deepseek` | DeepSeek | deepseek-chat | `DEEPSEEK_API_KEY` | 国产高性价比,¥1/百万token |
|
|
508
|
+
| 🧠 `deepseek-r1` | DeepSeek | deepseek-reasoner | `DEEPSEEK_API_KEY` | 推理增强,适合复杂漏洞分析 |
|
|
509
|
+
| `openai` | OpenAI | gpt-4o | `OPENAI_API_KEY` | GPT-4o 多模态旗舰 |
|
|
510
|
+
| 🧠 `openai-o3` | OpenAI | o3-mini | `OPENAI_API_KEY` | 推理型,速度快 |
|
|
511
|
+
| `qwen` | 阿里云 | qwen-plus | `DASHSCOPE_API_KEY` | 中文能力强,¥0.8/百万token |
|
|
512
|
+
| ⭐ `qwen-coder` | 阿里云 | qwen-coder-plus | `DASHSCOPE_API_KEY` | 代码专精,¥2/百万token |
|
|
513
|
+
| `claude` | Anthropic | claude-sonnet-4-20250514 | `ANTHROPIC_API_KEY` | 代码理解深度最强 |
|
|
514
|
+
| `gemini` | Google | gemini-2.5-pro | `GEMINI_API_KEY` | 100万token上下文 |
|
|
515
|
+
| `zhipu` | 智谱 AI | glm-4-plus | `ZHIPU_API_KEY` | 国产强推理 |
|
|
516
|
+
| `moonshot` | 月之暗面 | moonshot-v1-8k | `MOONSHOT_API_KEY` | 长文本处理强 |
|
|
517
|
+
| `codestral` | Mistral | codestral-latest | `MISTRAL_API_KEY` | 专注代码生成与审查 |
|
|
518
|
+
|
|
519
|
+
> ⭐ = 推荐 | 🧠 = 推理增强型
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
### coderev setup(模型配置)🆕 v1.0.24
|
|
524
|
+
|
|
525
|
+
**作用**:一键配置 AI 模型,支持主模型、从模型(fallback)、以及不同 Agent 使用不同模型。
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
coderev setup [选项]
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**参数**:
|
|
532
|
+
|
|
533
|
+
| 参数 | 说明 | 示例 |
|
|
534
|
+
|------|------|------|
|
|
535
|
+
| `--model <模板名>` | 设置主模型 | `coderev setup --model deepseek` |
|
|
536
|
+
| `--fallback <模板名>` | 设置备用模型(主模型失败时自动切换) | `coderev setup --model deepseek --fallback qwen` |
|
|
537
|
+
| `--agent-security <模板名>` | 安全 Agent 专用模型 | `coderev setup --agent-security deepseek-r1` |
|
|
538
|
+
| `--agent-bugs <模板名>` | 缺陷检测 Agent 专用模型 | `coderev setup --agent-bugs qwen-coder` |
|
|
539
|
+
| `--agent-quality <模板名>` | 质量检查 Agent 专用模型 | `coderev setup --agent-quality gemini` |
|
|
540
|
+
| (无参数) | 查看当前配置 | `coderev setup` |
|
|
541
|
+
|
|
542
|
+
**使用示例**:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
# 最简:选择 DeepSeek
|
|
546
|
+
coderev setup --model deepseek
|
|
547
|
+
|
|
548
|
+
# 主从模型:DeepSeek 挂了自动切到千问
|
|
549
|
+
coderev setup --model deepseek --fallback qwen
|
|
550
|
+
|
|
551
|
+
# 不同 Agent 不同模型
|
|
552
|
+
coderev setup --model deepseek --agent-security deepseek-r1 --agent-quality qwen
|
|
553
|
+
|
|
554
|
+
# 查看当前配置
|
|
555
|
+
coderev setup
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**主从模型回退**:主模型调用失败(超时/网络/API 错误)时,自动切到从模型,控制台打印 `↩ Falling back to xxx`。
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
### coderev rules(规则市场)🆕 v1.0.23
|
|
563
|
+
|
|
564
|
+
**作用**:访问 coderev SaaS 规则市场,搜索、安装、发布团队共享的审查规则包。
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
coderev rules <action> [选项]
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**子命令**:
|
|
571
|
+
|
|
572
|
+
| 动作 | 说明 | 示例 |
|
|
573
|
+
|------|------|------|
|
|
574
|
+
| `search [query]` | 搜索规则市场 | `coderev rules search react` |
|
|
575
|
+
| `install <name>` | 安装规则包 | `coderev rules install react-security` |
|
|
576
|
+
| `publish` | 发布本地规则到市场 | `coderev rules publish --name my-rules` |
|
|
577
|
+
| `list` | 查看已安装的规则包 | `coderev rules list` |
|
|
578
|
+
| `uninstall <name>` | 卸载规则包 | `coderev rules uninstall react-security` |
|
|
579
|
+
| `info <name>` | 查看规则包详情 | `coderev rules info react-security` |
|
|
580
|
+
|
|
581
|
+
**参数**:
|
|
582
|
+
|
|
583
|
+
| 参数 | 说明 |
|
|
584
|
+
|------|------|
|
|
585
|
+
| `-q, --query <text>` | 搜索查询 |
|
|
586
|
+
| `-n, --name <name>` | 规则包名称 |
|
|
587
|
+
| `--version <ver>` | 发布版本号(默认 1.0.0) |
|
|
588
|
+
| `--desc <text>` | 发布描述 |
|
|
589
|
+
| `--api-url <url>` | 自定义市场 API 地址 |
|
|
590
|
+
|
|
591
|
+
**使用示例**:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
# 搜索规则
|
|
595
|
+
coderev rules search react
|
|
596
|
+
|
|
597
|
+
# 安装规则包(自动合并到 .coderevrc.json)
|
|
598
|
+
coderev rules install react-security
|
|
599
|
+
|
|
600
|
+
# 查看已安装
|
|
601
|
+
coderev rules list
|
|
602
|
+
|
|
603
|
+
# 发布当地规则
|
|
604
|
+
coderev rules publish --name team-js-rules --desc "团队JS规范"
|
|
605
|
+
|
|
606
|
+
# 卸载
|
|
607
|
+
coderev rules uninstall react-security
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
安装后规则写入 `.coderevrc.json` 的 `rules.custom` 数组,带 `_source` 标记。
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
### coderev doctor(环境诊断)🆕 v1.0.26
|
|
615
|
+
|
|
616
|
+
**作用**:一键诊断 coderev 的运行环境,快速定位配置问题。
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
coderev doctor
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**检查项目**:
|
|
623
|
+
|
|
624
|
+
| 检查项 | 说明 |
|
|
625
|
+
|--------|------|
|
|
626
|
+
| Node.js Version | 检查 Node.js 版本是否 >= 18 |
|
|
627
|
+
| Git | 检查 git 是否安装及当前目录是否为 git 仓库 |
|
|
628
|
+
| Config File | 检查配置文件是否存在且格式有效 |
|
|
629
|
+
| API Key | 检查 API Key 是否已配置(环境变量或配置文件) |
|
|
630
|
+
| AI Provider Connectivity | 检查 AI 服务商网络连通性及 API 可用性 |
|
|
631
|
+
|
|
632
|
+
**参数**:
|
|
633
|
+
|
|
634
|
+
| 参数 | 说明 | 示例 |
|
|
635
|
+
|------|------|------|
|
|
636
|
+
| `-c, --config <path>` | 指定配置文件路径 | `coderev doctor --config ./my-config.json` |
|
|
637
|
+
| `--json` | 以 JSON 格式输出 | `coderev doctor --json` |
|
|
638
|
+
|
|
639
|
+
**输出示例**:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
🩺 coderev Doctor — Environment Diagnostic
|
|
643
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
644
|
+
|
|
645
|
+
✔ Node.js Version
|
|
646
|
+
v24.13.0 (>= 18 required)
|
|
647
|
+
|
|
648
|
+
✔ Git
|
|
649
|
+
git version 2.45.0 — inside a git repository
|
|
650
|
+
|
|
651
|
+
✔ Config File
|
|
652
|
+
Valid: /path/to/.coderevrc.json
|
|
653
|
+
|
|
654
|
+
✔ API Key
|
|
655
|
+
Found from env $DEEPSEEK_API_KEY: sk-xxxx...xxxx
|
|
656
|
+
|
|
657
|
+
✔ AI Provider Connectivity
|
|
658
|
+
Connected to deepseek (https://api.deepseek.com) — API accessible
|
|
659
|
+
|
|
660
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
661
|
+
5 passed
|
|
662
|
+
|
|
663
|
+
✔ All checks passed! Your environment is ready for coderev. 🚀
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**适用场景**:
|
|
667
|
+
- 首次安装后验证环境是否就绪
|
|
668
|
+
- 遇到 `API key not found` 等错误时快速定位
|
|
669
|
+
- 切换 AI 服务商后检查连通性
|
|
670
|
+
- CI 环境中验证配置是否完整
|
|
671
|
+
|
|
672
|
+
`doctor` 命令返回非零退出码当有检查项失败(`✖`),适合在 CI 脚本中使用。
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
484
676
|
## 配置详解
|
|
485
677
|
|
|
486
678
|
### 配置加载顺序
|
|
@@ -815,6 +1007,55 @@ git diff origin/main...HEAD | coderev review --ci --min-confidence 70
|
|
|
815
1007
|
|
|
816
1008
|
---
|
|
817
1009
|
|
|
1010
|
+
有关 GitHub Actions、GitLab CI、Jenkins 的 CI/CD 集成说明及模板,见 [docs/ci-cd.md](docs/ci-cd.md)。
|
|
1011
|
+
|
|
1012
|
+
一键生成对应 CI 模板:
|
|
1013
|
+
|
|
1014
|
+
```bash
|
|
1015
|
+
# GitHub Actions 模板
|
|
1016
|
+
coderev init --github-action
|
|
1017
|
+
# → 生成 .github/workflows/coderev.yml
|
|
1018
|
+
|
|
1019
|
+
# GitLab CI 模板
|
|
1020
|
+
coderev init --gitlab-ci
|
|
1021
|
+
# → 生成 .gitlab-ci.yml
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
## VS Code 扩展 🆕 v1.0.22
|
|
1027
|
+
|
|
1028
|
+
coderev 自带 VS Code 扩展,安装后在编码时实时审查代码。
|
|
1029
|
+
|
|
1030
|
+
### 安装
|
|
1031
|
+
|
|
1032
|
+
```bash
|
|
1033
|
+
# 从本地安装(源码已包含在 vscode/ 目录中)
|
|
1034
|
+
cd vscode && npm install && npm run package
|
|
1035
|
+
# 在 VS Code 中:Ctrl+Shift+P → Extensions: Install from VSIX...
|
|
1036
|
+
# 选择 vscode/coderev-*.vsix
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
> 暂未上架 VS Code Marketplace,通过本地 VSIX 安装。
|
|
1040
|
+
|
|
1041
|
+
### 功能
|
|
1042
|
+
|
|
1043
|
+
| 功能 | 说明 |
|
|
1044
|
+
|------|------|
|
|
1045
|
+
| 工作区审查 | 右键项目目录 → `coderev: Review Workspace` |
|
|
1046
|
+
| 单文件审查 | 打开文件 → `Ctrl+Shift+P` → `coderev: Review File` |
|
|
1047
|
+
| 自动修复 | 审查发现问题后 → 一键调用 AI 修复 |
|
|
1048
|
+
| 保存即审查 | 开启 `coderev.lintOnSave` → 每次保存文件自动审查 |
|
|
1049
|
+
| Problems 面板集成 | 问题自动输出到 VS Code Problems 面板,点击跳转 |
|
|
1050
|
+
|
|
1051
|
+
### 扩展设置
|
|
1052
|
+
|
|
1053
|
+
- `coderev.enabled` — 是否启用自动检查(默认 true)
|
|
1054
|
+
- `coderev.lintOnSave` — 保存时审查(默认 false)
|
|
1055
|
+
- `coderev.minConfidence` — 最低置信度阈值(默认 60)
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
818
1059
|
## FAQ / 常见问题
|
|
819
1060
|
|
|
820
1061
|
### Q:为什么审查结果为空?
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderev-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "Multi-agent AI code review for git -- parallel agents with confidence scoring",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"coderev": "src/
|
|
7
|
+
"coderev": "src/bin.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/cli.js",
|
package/src/bin.js
ADDED
package/src/cli.js
CHANGED
|
@@ -1078,6 +1078,33 @@ program
|
|
|
1078
1078
|
}
|
|
1079
1079
|
});
|
|
1080
1080
|
|
|
1081
|
+
// ── Doctor (Environment Diagnostic) ─────────────────────────────
|
|
1082
|
+
program
|
|
1083
|
+
.command('doctor')
|
|
1084
|
+
.description('Run environment diagnostic to troubleshoot common issues')
|
|
1085
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
1086
|
+
.option('--json', 'Output as JSON')
|
|
1087
|
+
.action(async (options) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const { runDoctor, formatDoctorReport } = require('./doctor');
|
|
1090
|
+
console.error(chalk.blue('↻ Running environment diagnostic...\n'));
|
|
1091
|
+
const { checks, allPassed } = await runDoctor({ config: options.config });
|
|
1092
|
+
|
|
1093
|
+
if (options.json) {
|
|
1094
|
+
console.log(JSON.stringify({ allPassed, checks }, null, 2));
|
|
1095
|
+
} else {
|
|
1096
|
+
console.log(formatDoctorReport(checks));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (!allPassed) {
|
|
1100
|
+
process.exitCode = 1;
|
|
1101
|
+
}
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1081
1108
|
program.parse(process.argv);
|
|
1082
1109
|
|
|
1083
1110
|
// ── Helpers ───────────────────────────────────────────────────
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coderev doctor — 环境诊断命令
|
|
3
|
+
*
|
|
4
|
+
* 诊断项目环境中的常见配置问题:
|
|
5
|
+
* 1. Node.js 版本检查
|
|
6
|
+
* 2. git 可用性检查
|
|
7
|
+
* 3. 配置文件有效性检查
|
|
8
|
+
* 4. API Key 配置检查
|
|
9
|
+
* 5. AI Provider 网络连通性检查
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
|
|
18
|
+
// ── 检查项类型 ────────────────────────────────────────────────
|
|
19
|
+
const PASS = 'pass';
|
|
20
|
+
const WARN = 'warn';
|
|
21
|
+
const FAIL = 'fail';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 执行完整的诊断流程并返回结果
|
|
25
|
+
*
|
|
26
|
+
* @param {object} options - 选项
|
|
27
|
+
* @param {string} [options.config] - 显式指定配置文件路径
|
|
28
|
+
* @returns {Promise<{checks: Array, allPassed: boolean}>}
|
|
29
|
+
*/
|
|
30
|
+
async function runDoctor(options = {}) {
|
|
31
|
+
const checks = [];
|
|
32
|
+
const config = loadUserConfig(options.config);
|
|
33
|
+
|
|
34
|
+
// 1. Node.js 版本
|
|
35
|
+
checks.push(checkNodeVersion());
|
|
36
|
+
|
|
37
|
+
// 2. Git 可用性
|
|
38
|
+
checks.push(checkGit());
|
|
39
|
+
|
|
40
|
+
// 3. 配置文件
|
|
41
|
+
checks.push(checkConfig(options.config));
|
|
42
|
+
|
|
43
|
+
// 4. API Key
|
|
44
|
+
checks.push(checkApiKey(config));
|
|
45
|
+
|
|
46
|
+
// 5. AI Provider 连通性
|
|
47
|
+
checks.push(await checkProviderConnectivity(config));
|
|
48
|
+
|
|
49
|
+
// 判断是否全部通过
|
|
50
|
+
const allPassed = checks.every(c => c.status !== FAIL);
|
|
51
|
+
|
|
52
|
+
return { checks, allPassed };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── 各检查项 ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 检查 Node.js 版本 >= 18
|
|
59
|
+
*/
|
|
60
|
+
function checkNodeVersion() {
|
|
61
|
+
const version = process.version;
|
|
62
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
63
|
+
|
|
64
|
+
if (major >= 20) {
|
|
65
|
+
return {
|
|
66
|
+
name: 'Node.js Version',
|
|
67
|
+
status: PASS,
|
|
68
|
+
message: `${version} (>= 18 required)`,
|
|
69
|
+
detail: `Node.js version is current and fully supported.`,
|
|
70
|
+
};
|
|
71
|
+
} else if (major >= 18) {
|
|
72
|
+
return {
|
|
73
|
+
name: 'Node.js Version',
|
|
74
|
+
status: PASS,
|
|
75
|
+
message: `${version} (minimum met)`,
|
|
76
|
+
detail: `Node.js >= 18 required. Your version meets the minimum.`,
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
return {
|
|
80
|
+
name: 'Node.js Version',
|
|
81
|
+
status: FAIL,
|
|
82
|
+
message: `${version} is too old — Node.js >= 18 required`,
|
|
83
|
+
detail: `Upgrade to Node.js 18+ to use coderev. Download: https://nodejs.org`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 检查 git 是否可用
|
|
90
|
+
*/
|
|
91
|
+
function checkGit() {
|
|
92
|
+
try {
|
|
93
|
+
const gitVersion = execSync('git --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
94
|
+
// Verify we're in a git repo (optional, warn if not)
|
|
95
|
+
let isRepo = true;
|
|
96
|
+
try {
|
|
97
|
+
execSync('git rev-parse --git-dir', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
98
|
+
} catch {
|
|
99
|
+
isRepo = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isRepo) {
|
|
103
|
+
return {
|
|
104
|
+
name: 'Git',
|
|
105
|
+
status: PASS,
|
|
106
|
+
message: `${gitVersion} — inside a git repository`,
|
|
107
|
+
detail: `Git is available and current directory is a git repo.`,
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return {
|
|
111
|
+
name: 'Git',
|
|
112
|
+
status: PASS,
|
|
113
|
+
message: `${gitVersion} — not in a git repository`,
|
|
114
|
+
detail: `Git is available but current directory is not a git repo. This is fine for reviewing diff files directly.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
return {
|
|
119
|
+
name: 'Git',
|
|
120
|
+
status: FAIL,
|
|
121
|
+
message: 'git not found in PATH',
|
|
122
|
+
detail: `Git is required for most coderev operations. Install: https://git-scm.com`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 检查配置文件是否存在且格式有效
|
|
129
|
+
*/
|
|
130
|
+
function checkConfig(explicitPath) {
|
|
131
|
+
if (explicitPath) {
|
|
132
|
+
if (!fs.existsSync(explicitPath)) {
|
|
133
|
+
return {
|
|
134
|
+
name: 'Config File',
|
|
135
|
+
status: WARN,
|
|
136
|
+
message: `Specified config not found: ${explicitPath}`,
|
|
137
|
+
detail: `The config file specified via --config does not exist. coderev will use defaults.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
JSON.parse(fs.readFileSync(explicitPath, 'utf-8'));
|
|
142
|
+
return {
|
|
143
|
+
name: 'Config File',
|
|
144
|
+
status: PASS,
|
|
145
|
+
message: `Valid: ${explicitPath}`,
|
|
146
|
+
detail: `Config file exists and contains valid JSON.`,
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
name: 'Config File',
|
|
151
|
+
status: FAIL,
|
|
152
|
+
message: `Invalid JSON: ${err.message}`,
|
|
153
|
+
detail: `Fix the JSON syntax in your config file.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Search for config in cwd and parents
|
|
159
|
+
const configFiles = ['.coderevrc.json', '.coderevrc', 'coderev.config.json'];
|
|
160
|
+
let found = null;
|
|
161
|
+
let current = process.cwd();
|
|
162
|
+
|
|
163
|
+
while (true) {
|
|
164
|
+
for (const name of configFiles) {
|
|
165
|
+
const full = path.join(current, name);
|
|
166
|
+
if (fs.existsSync(full)) {
|
|
167
|
+
found = full;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (found) break;
|
|
172
|
+
const parent = path.dirname(current);
|
|
173
|
+
if (parent === current) break;
|
|
174
|
+
current = parent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (found) {
|
|
178
|
+
try {
|
|
179
|
+
const content = JSON.parse(fs.readFileSync(found, 'utf-8'));
|
|
180
|
+
// Check basic structure
|
|
181
|
+
const warnings = [];
|
|
182
|
+
if (!content.ai) warnings.push('Missing "ai" section');
|
|
183
|
+
else {
|
|
184
|
+
if (!content.ai.provider && !content.ai.model && !content.ai.apiKey) {
|
|
185
|
+
warnings.push('"ai" section has no provider, model, or apiKey — review may not work');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (warnings.length > 0) {
|
|
190
|
+
return {
|
|
191
|
+
name: 'Config File',
|
|
192
|
+
status: WARN,
|
|
193
|
+
message: `Found at ${found} but with issues`,
|
|
194
|
+
detail: warnings.join('; '),
|
|
195
|
+
warnings,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
name: 'Config File',
|
|
201
|
+
status: PASS,
|
|
202
|
+
message: `Valid: ${found}`,
|
|
203
|
+
detail: `Config file found and contains valid JSON with required sections.`,
|
|
204
|
+
};
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
name: 'Config File',
|
|
208
|
+
status: FAIL,
|
|
209
|
+
message: `Invalid JSON in ${found}: ${err.message}`,
|
|
210
|
+
detail: `Fix the JSON syntax in your config file.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
name: 'Config File',
|
|
217
|
+
status: WARN,
|
|
218
|
+
message: 'No config file found',
|
|
219
|
+
detail: `No .coderevrc.json found in current or parent directories. Run "coderev init" to create one.`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 检查 API Key 是否已配置
|
|
225
|
+
*/
|
|
226
|
+
function checkApiKey(config) {
|
|
227
|
+
// Direct key
|
|
228
|
+
if (config.ai?.apiKey) {
|
|
229
|
+
const key = config.ai.apiKey;
|
|
230
|
+
const masked = key.slice(0, 8) + '...' + key.slice(-4);
|
|
231
|
+
return {
|
|
232
|
+
name: 'API Key',
|
|
233
|
+
status: PASS,
|
|
234
|
+
message: `Configured in config file: ${masked}`,
|
|
235
|
+
detail: `API key is set directly in the config file.`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Via environment variable
|
|
240
|
+
const envVar = config.ai?.apiKeyEnv || 'OPENAI_API_KEY';
|
|
241
|
+
const key = process.env[envVar];
|
|
242
|
+
if (key) {
|
|
243
|
+
const masked = key.slice(0, 8) + '...' + key.slice(-4);
|
|
244
|
+
return {
|
|
245
|
+
name: 'API Key',
|
|
246
|
+
status: PASS,
|
|
247
|
+
message: `Found from env $${envVar}: ${masked}`,
|
|
248
|
+
detail: `API key loaded from environment variable ${envVar}.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Try common env vars
|
|
253
|
+
const commonVars = ['DEEPSEEK_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'DASHSCOPE_API_KEY', 'GEMINI_API_KEY', 'ZHIPU_API_KEY', 'MOONSHOT_API_KEY', 'MISTRAL_API_KEY'];
|
|
254
|
+
for (const v of commonVars) {
|
|
255
|
+
if (process.env[v]) {
|
|
256
|
+
return {
|
|
257
|
+
name: 'API Key',
|
|
258
|
+
status: PASS,
|
|
259
|
+
message: `Found from env $${v} (not configured in config, but available)`,
|
|
260
|
+
detail: `API key found in environment variable ${v}. Consider adding "apiKeyEnv" to your config.`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
name: 'API Key',
|
|
267
|
+
status: FAIL,
|
|
268
|
+
message: `No API key found`,
|
|
269
|
+
detail: `Set your API key via environment variable (e.g. DEEPSEEK_API_KEY) or in .coderevrc.json. Run "coderev init" and "coderev setup --model deepseek" to get started.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 检查 AI Provider 网络连通性
|
|
275
|
+
*/
|
|
276
|
+
async function checkProviderConnectivity(config) {
|
|
277
|
+
const provider = config.ai?.provider || 'deepseek';
|
|
278
|
+
const baseURL = config.ai?.baseURL || getDefaultBaseURL(provider);
|
|
279
|
+
|
|
280
|
+
// Check general internet connectivity first
|
|
281
|
+
const internetOk = await checkInternet();
|
|
282
|
+
if (!internetOk) {
|
|
283
|
+
// If we have no API key at all, this might be expected
|
|
284
|
+
const key = config.ai?.apiKey || process.env[config.ai?.apiKeyEnv || 'DEEPSEEK_API_KEY'];
|
|
285
|
+
if (!key) {
|
|
286
|
+
return {
|
|
287
|
+
name: 'AI Provider Connectivity',
|
|
288
|
+
status: WARN,
|
|
289
|
+
message: 'Cannot check connectivity — no API key configured',
|
|
290
|
+
detail: `Set up your API key first, then re-run "coderev doctor" to verify connectivity.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
name: 'AI Provider Connectivity',
|
|
295
|
+
status: FAIL,
|
|
296
|
+
message: 'No internet connectivity detected',
|
|
297
|
+
detail: `Cannot reach external servers. Check your network connection and proxy settings.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!baseURL || baseURL === 'unknown') {
|
|
302
|
+
return {
|
|
303
|
+
name: 'AI Provider Connectivity',
|
|
304
|
+
status: WARN,
|
|
305
|
+
message: `Unknown base URL for provider "${provider}"`,
|
|
306
|
+
detail: `Cannot determine the API endpoint for this provider. Check your config's "baseURL" setting.`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const url = new URL(baseURL);
|
|
312
|
+
const reachable = await httpHead(url.origin);
|
|
313
|
+
|
|
314
|
+
if (reachable) {
|
|
315
|
+
// Try API models endpoint to verify auth works
|
|
316
|
+
const apiKey = config.ai?.apiKey || process.env[config.ai?.apiKeyEnv || 'DEEPSEEK_API_KEY'];
|
|
317
|
+
if (apiKey) {
|
|
318
|
+
const apiOk = await checkApiEndpoint(url.origin, '/models', apiKey);
|
|
319
|
+
if (apiOk) {
|
|
320
|
+
return {
|
|
321
|
+
name: 'AI Provider Connectivity',
|
|
322
|
+
status: PASS,
|
|
323
|
+
message: `Connected to ${provider} (${url.origin}) — API accessible`,
|
|
324
|
+
detail: `Successfully connected to the AI provider's API endpoint with valid authentication.`,
|
|
325
|
+
};
|
|
326
|
+
} else {
|
|
327
|
+
return {
|
|
328
|
+
name: 'AI Provider Connectivity',
|
|
329
|
+
status: WARN,
|
|
330
|
+
message: `${provider} (${url.origin}) is reachable but API returned an error`,
|
|
331
|
+
detail: `The server is reachable but the /models endpoint returned an error. Your API key may be invalid or the endpoint URL may be incorrect.`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
name: 'AI Provider Connectivity',
|
|
338
|
+
status: PASS,
|
|
339
|
+
message: `Connected to ${provider} (${url.origin}) — server reachable`,
|
|
340
|
+
detail: `The AI provider's server is reachable. Full API auth check skipped (no API key to verify).`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
name: 'AI Provider Connectivity',
|
|
346
|
+
status: FAIL,
|
|
347
|
+
message: `Cannot reach ${provider} at ${url.origin}`,
|
|
348
|
+
detail: `The AI provider's server is not responding. Check your network, firewall, or proxy settings. Some providers may be blocked in certain regions.`,
|
|
349
|
+
};
|
|
350
|
+
} catch (err) {
|
|
351
|
+
return {
|
|
352
|
+
name: 'AI Provider Connectivity',
|
|
353
|
+
status: FAIL,
|
|
354
|
+
message: `Invalid base URL: ${baseURL} (${err.message})`,
|
|
355
|
+
detail: `The configured baseURL is not a valid URL. Fix the "baseURL" setting in your config.`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Helper Functions ──────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Load user config (simple, without inheritance)
|
|
364
|
+
*/
|
|
365
|
+
function loadUserConfig(explicitPath) {
|
|
366
|
+
if (explicitPath) {
|
|
367
|
+
if (fs.existsSync(explicitPath)) {
|
|
368
|
+
try {
|
|
369
|
+
return JSON.parse(fs.readFileSync(explicitPath, 'utf-8'));
|
|
370
|
+
} catch {
|
|
371
|
+
return {};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Search for config
|
|
378
|
+
const configFiles = ['.coderevrc.json', '.coderevrc', 'coderev.config.json'];
|
|
379
|
+
let current = process.cwd();
|
|
380
|
+
|
|
381
|
+
while (true) {
|
|
382
|
+
for (const name of configFiles) {
|
|
383
|
+
const full = path.join(current, name);
|
|
384
|
+
if (fs.existsSync(full)) {
|
|
385
|
+
try {
|
|
386
|
+
return JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
387
|
+
} catch {
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const parent = path.dirname(current);
|
|
393
|
+
if (parent === current) break;
|
|
394
|
+
current = parent;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get default base URL for known providers
|
|
402
|
+
*/
|
|
403
|
+
function getDefaultBaseURL(provider) {
|
|
404
|
+
const urls = {
|
|
405
|
+
openai: 'https://api.openai.com/v1',
|
|
406
|
+
deepseek: 'https://api.deepseek.com/v1',
|
|
407
|
+
anthropic: 'https://api.anthropic.com',
|
|
408
|
+
dashscope: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
409
|
+
qwen: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
410
|
+
gemini: 'https://generativelanguage.googleapis.com',
|
|
411
|
+
zhipu: 'https://open.bigmodel.cn/api/paas/v4',
|
|
412
|
+
moonshot: 'https://api.moonshot.cn/v1',
|
|
413
|
+
mistral: 'https://api.mistral.ai/v1',
|
|
414
|
+
};
|
|
415
|
+
return urls[provider.toLowerCase()] || 'unknown';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check basic internet connectivity
|
|
420
|
+
*/
|
|
421
|
+
function checkInternet() {
|
|
422
|
+
return new Promise((resolve) => {
|
|
423
|
+
const req = https.get('https://www.google.com', { timeout: 5000 }, () => {
|
|
424
|
+
resolve(true);
|
|
425
|
+
});
|
|
426
|
+
req.on('error', () => {
|
|
427
|
+
// Try another host
|
|
428
|
+
const req2 = https.get('https://api.github.com', { timeout: 5000 }, () => {
|
|
429
|
+
resolve(true);
|
|
430
|
+
});
|
|
431
|
+
req2.on('error', () => resolve(false));
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Simple HTTP HEAD check
|
|
438
|
+
*/
|
|
439
|
+
function httpHead(origin) {
|
|
440
|
+
return new Promise((resolve) => {
|
|
441
|
+
const url = new URL(origin);
|
|
442
|
+
const options = {
|
|
443
|
+
hostname: url.hostname,
|
|
444
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
445
|
+
path: '/',
|
|
446
|
+
method: 'HEAD',
|
|
447
|
+
timeout: 8000,
|
|
448
|
+
rejectUnauthorized: false,
|
|
449
|
+
};
|
|
450
|
+
const mod = url.protocol === 'https:' ? https : require('http');
|
|
451
|
+
const req = mod.request(options, (res) => {
|
|
452
|
+
resolve(true);
|
|
453
|
+
res.resume();
|
|
454
|
+
});
|
|
455
|
+
req.on('error', () => resolve(false));
|
|
456
|
+
req.on('timeout', () => {
|
|
457
|
+
req.destroy();
|
|
458
|
+
resolve(false);
|
|
459
|
+
});
|
|
460
|
+
req.end();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if an API endpoint is accessible with auth
|
|
466
|
+
*/
|
|
467
|
+
function checkApiEndpoint(origin, path, apiKey) {
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
const url = new URL(origin + path);
|
|
470
|
+
const options = {
|
|
471
|
+
hostname: url.hostname,
|
|
472
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
473
|
+
path: url.pathname,
|
|
474
|
+
method: 'GET',
|
|
475
|
+
timeout: 10000,
|
|
476
|
+
headers: {
|
|
477
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
478
|
+
'User-Agent': `coderev-doctor/${require('../package.json').version}`,
|
|
479
|
+
},
|
|
480
|
+
rejectUnauthorized: false,
|
|
481
|
+
};
|
|
482
|
+
const mod = url.protocol === 'https:' ? https : require('http');
|
|
483
|
+
const req = mod.request(options, (res) => {
|
|
484
|
+
// 2xx or 4xx (even 401 means the endpoint is accessible, just auth issue)
|
|
485
|
+
resolve(res.statusCode < 500);
|
|
486
|
+
res.resume();
|
|
487
|
+
});
|
|
488
|
+
req.on('error', () => resolve(false));
|
|
489
|
+
req.on('timeout', () => {
|
|
490
|
+
req.destroy();
|
|
491
|
+
resolve(false);
|
|
492
|
+
});
|
|
493
|
+
req.end();
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── 格式化输出 ────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Format the diagnostic result as a colored terminal report
|
|
501
|
+
*/
|
|
502
|
+
function formatDoctorReport(checks) {
|
|
503
|
+
const lines = [];
|
|
504
|
+
|
|
505
|
+
lines.push('');
|
|
506
|
+
lines.push(chalk.bold('🩺 coderev Doctor — Environment Diagnostic'));
|
|
507
|
+
lines.push(chalk.gray('━'.repeat(58)));
|
|
508
|
+
|
|
509
|
+
let passCount = 0;
|
|
510
|
+
let warnCount = 0;
|
|
511
|
+
let failCount = 0;
|
|
512
|
+
|
|
513
|
+
for (const check of checks) {
|
|
514
|
+
const icon = check.status === PASS ? chalk.green('✔') :
|
|
515
|
+
check.status === WARN ? chalk.yellow('⚠') :
|
|
516
|
+
chalk.red('✖');
|
|
517
|
+
|
|
518
|
+
lines.push('');
|
|
519
|
+
lines.push(` ${icon} ${chalk.bold(check.name)}`);
|
|
520
|
+
lines.push(` ${colorStatus(check.status, check.message)}`);
|
|
521
|
+
|
|
522
|
+
if (check.detail) {
|
|
523
|
+
const detailColor = check.status === PASS ? chalk.gray :
|
|
524
|
+
check.status === WARN ? chalk.yellow :
|
|
525
|
+
chalk.red;
|
|
526
|
+
lines.push(` ${detailColor(check.detail)}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (check.warnings && check.warnings.length > 0) {
|
|
530
|
+
for (const w of check.warnings) {
|
|
531
|
+
lines.push(` ${chalk.yellow(' → ' + w)}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (check.status === PASS) passCount++;
|
|
536
|
+
else if (check.status === WARN) warnCount++;
|
|
537
|
+
else failCount++;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Summary
|
|
541
|
+
lines.push('');
|
|
542
|
+
lines.push(chalk.gray('━'.repeat(58)));
|
|
543
|
+
const summaryParts = [];
|
|
544
|
+
if (passCount > 0) summaryParts.push(chalk.green(`${passCount} passed`));
|
|
545
|
+
if (warnCount > 0) summaryParts.push(chalk.yellow(`${warnCount} warnings`));
|
|
546
|
+
if (failCount > 0) summaryParts.push(chalk.red(`${failCount} failed`));
|
|
547
|
+
lines.push(` ${summaryParts.join(' ')}`);
|
|
548
|
+
|
|
549
|
+
if (failCount > 0) {
|
|
550
|
+
lines.push('');
|
|
551
|
+
lines.push(chalk.red(' ✖ Some checks failed. Fix the issues above before using coderev.'));
|
|
552
|
+
} else if (warnCount > 0) {
|
|
553
|
+
lines.push('');
|
|
554
|
+
lines.push(chalk.yellow(' ⚠ Some checks have warnings. coderev will work, but may be suboptimal.'));
|
|
555
|
+
} else {
|
|
556
|
+
lines.push('');
|
|
557
|
+
lines.push(chalk.green(' ✔ All checks passed! Your environment is ready for coderev. 🚀'));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
lines.push('');
|
|
561
|
+
return lines.join('\n');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Color a status message
|
|
566
|
+
*/
|
|
567
|
+
function colorStatus(status, message) {
|
|
568
|
+
if (status === PASS) return chalk.green(message);
|
|
569
|
+
if (status === WARN) return chalk.yellow(message);
|
|
570
|
+
return chalk.red(message);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
module.exports = { runDoctor, formatDoctorReport, PASS, WARN, FAIL };
|