coderev-cli 1.0.25 → 1.1.0
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 +838 -0
- package/package.json +1 -1
- package/src/cli.js +110 -1
- package/src/doctor.js +573 -0
- package/src/models.js +59 -0
- package/src/models.test.js +139 -2
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 };
|
package/src/models.js
CHANGED
|
@@ -152,9 +152,68 @@ function getTemplate(name) {
|
|
|
152
152
|
return BUILTIN_TEMPLATES[name] || null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Priority order for auto-detection:
|
|
157
|
+
* recommended tier first, then standard tier
|
|
158
|
+
*/
|
|
159
|
+
const AUTO_DETECT_PRIORITY = [
|
|
160
|
+
'deepseek',
|
|
161
|
+
'qwen-coder',
|
|
162
|
+
'qwen',
|
|
163
|
+
'openai',
|
|
164
|
+
'claude',
|
|
165
|
+
'gemini',
|
|
166
|
+
'zhipu',
|
|
167
|
+
'moonshot',
|
|
168
|
+
'codestral',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Auto-detect the best available AI provider by scanning environment
|
|
173
|
+
* variables for known API keys.
|
|
174
|
+
*
|
|
175
|
+
* Scans all built-in template apiKeyEnv vars and returns the first
|
|
176
|
+
* matching template in priority order (recommended tier first).
|
|
177
|
+
*
|
|
178
|
+
* @returns {{ template: object, name: string, detected: string[], allDetected: string[] } | null}
|
|
179
|
+
*/
|
|
180
|
+
function autoDetectProvider() {
|
|
181
|
+
const detected = [];
|
|
182
|
+
|
|
183
|
+
// Scan all templates for available API keys
|
|
184
|
+
for (const [name, t] of Object.entries(BUILTIN_TEMPLATES)) {
|
|
185
|
+
if (process.env[t.apiKeyEnv]) {
|
|
186
|
+
detected.push(name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (detected.length === 0) return null;
|
|
191
|
+
|
|
192
|
+
// Pick the highest-priority detected template
|
|
193
|
+
let chosen = null;
|
|
194
|
+
for (const name of AUTO_DETECT_PRIORITY) {
|
|
195
|
+
if (detected.includes(name)) {
|
|
196
|
+
chosen = name;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Fallback: if default priority list missed something, pick first detected
|
|
202
|
+
if (!chosen) chosen = detected[0];
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
template: BUILTIN_TEMPLATES[chosen],
|
|
206
|
+
name: chosen,
|
|
207
|
+
chosen,
|
|
208
|
+
allDetected: detected,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
155
212
|
module.exports = {
|
|
156
213
|
BUILTIN_TEMPLATES,
|
|
157
214
|
resolveTemplate,
|
|
158
215
|
listTemplates,
|
|
159
216
|
getTemplate,
|
|
217
|
+
autoDetectProvider,
|
|
218
|
+
AUTO_DETECT_PRIORITY,
|
|
160
219
|
};
|