basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import process from 'node:process';8import os from 'node:os';9import { getNpmVersion, getGitVersion } from './systemInfo.js';10import { validateAuthMethod } from '../config/auth.js';11import {12 findProviderByCredentials,13 canUseRipgrep,14 getMCPServerStatus,15 MCPServerStatus,16} from '@qwen-code/qwen-code-core';17import type { CommandContext } from '../ui/commands/types.js';18import type { DoctorCheckResult } from '../ui/types.js';19import { t } from '../i18n/index.js';20 21const MIN_NODE_MAJOR = 22;22 23function checkNodeVersion(): DoctorCheckResult {24 const version = process.version;25 const major = parseInt(version.replace(/^v/, '').split('.')[0]!, 10);26 if (isNaN(major) || major < MIN_NODE_MAJOR) {27 return {28 category: t('System'),29 name: t('Node.js version'),30 status: 'fail',31 message: version,32 detail: t('Node.js v{{min}}+ is required. Current: {{version}}', {33 min: String(MIN_NODE_MAJOR),34 version,35 }),36 };37 }38 return {39 category: t('System'),40 name: t('Node.js version'),41 status: 'pass',42 message: version,43 };44}45 46async function checkNpmVersion(): Promise<DoctorCheckResult> {47 const version = await getNpmVersion();48 if (version === 'unknown') {49 return {50 category: t('System'),51 name: t('npm version'),52 status: 'warn',53 message: t('not found'),54 detail: t('npm is not available. Some features may not work.'),55 };56 }57 return {58 category: t('System'),59 name: t('npm version'),60 status: 'pass',61 message: version,62 };63}64 65function checkPlatform(): DoctorCheckResult {66 return {67 category: t('System'),68 name: t('Platform'),69 status: 'pass',70 message: `${process.platform}/${process.arch} (${os.release()})`,71 };72}73 74function checkAuth(context: CommandContext): DoctorCheckResult {75 const config = context.services.config;76 const authType = config?.getAuthType();77 if (!authType) {78 return {79 category: t('Authentication'),80 name: t('API key'),81 status: 'fail',82 message: t('not configured'),83 detail: t('Run /auth to configure authentication.'),84 };85 }86 87 const error = validateAuthMethod(authType, config ?? undefined);88 if (error) {89 return {90 category: t('Authentication'),91 name: t('API key'),92 status: 'fail',93 message: t('invalid ({{authType}})', { authType }),94 detail: error,95 };96 }97 98 // Build enriched diagnostic information99 const cgConfig =100 typeof config?.getContentGeneratorConfig === 'function'101 ? config.getContentGeneratorConfig()102 : undefined;103 const model = cgConfig?.model ?? config?.getModel();104 const baseUrl = cgConfig?.baseUrl;105 const envKey = cgConfig?.apiKeyEnvKey;106 107 const provider = findProviderByCredentials(baseUrl, envKey);108 109 const detailParts: string[] = [];110 if (provider) {111 detailParts.push(112 t('Provider: {{provider}}', { provider: t(provider.label) }),113 );114 }115 if (baseUrl) {116 detailParts.push(t('Base URL: {{baseUrl}}', { baseUrl }));117 }118 if (model) {119 detailParts.push(t('Model: {{model}}', { model }));120 }121 if (envKey) {122 const hasKey = !!process.env[envKey];123 detailParts.push(124 hasKey125 ? t('API key: configured ({{envKey}})', { envKey })126 : t('API key: {{envKey}} not set', { envKey }),127 );128 }129 130 return {131 category: t('Authentication'),132 name: t('API key'),133 status: 'pass',134 message: t('configured ({{authType}})', { authType }),135 detail: detailParts.length > 0 ? detailParts.join('\n') : undefined,136 };137}138 139async function checkApiClient(140 context: CommandContext,141): Promise<DoctorCheckResult> {142 const config = context.services.config;143 if (!config) {144 return {145 category: t('Authentication'),146 name: t('API client'),147 status: 'fail',148 message: t('config not loaded'),149 };150 }151 152 try {153 const client = config.getGeminiClient();154 if (client.isInitialized()) {155 return {156 category: t('Authentication'),157 name: t('API client'),158 status: 'pass',159 message: t('client initialized'),160 };161 }162 return {163 category: t('Authentication'),164 name: t('API client'),165 status: 'warn',166 message: t('client not initialized'),167 detail: t('The API client has not been initialized yet.'),168 };169 } catch (error) {170 const errorMsg = error instanceof Error ? error.message : String(error);171 return {172 category: t('Authentication'),173 name: t('API client'),174 status: 'warn',175 message: t('error'),176 detail: errorMsg,177 };178 }179}180 181function checkSettings(context: CommandContext): DoctorCheckResult {182 const settings = context.services.settings;183 if (!settings) {184 return {185 category: t('Configuration'),186 name: t('Settings'),187 status: 'fail',188 message: t('not loaded'),189 detail: t(190 'Settings could not be loaded. Check your settings files for syntax errors.',191 ),192 };193 }194 return {195 category: t('Configuration'),196 name: t('Settings'),197 status: 'pass',198 message: t('loaded'),199 };200}201 202function checkModel(context: CommandContext): DoctorCheckResult {203 const model = context.services.config?.getModel();204 if (!model) {205 return {206 category: t('Configuration'),207 name: t('Model'),208 status: 'fail',209 message: t('not configured'),210 detail: t('Run /model to select a model.'),211 };212 }213 return {214 category: t('Configuration'),215 name: t('Model'),216 status: 'pass',217 message: model,218 };219}220 221function checkMcpServers(context: CommandContext): DoctorCheckResult[] {222 const config = context.services.config;223 const servers = config?.getMcpServers();224 if (!servers || Object.keys(servers).length === 0) {225 return [226 {227 category: t('MCP Servers'),228 name: t('MCP servers'),229 status: 'pass',230 message: t('none configured'),231 },232 ];233 }234 235 // In non-interactive mode MCP connections are never established, so querying236 // getMCPServerStatus would always return DISCONNECTED and produce false failures.237 // Report configured servers as unchecked instead.238 if (context.executionMode !== 'interactive') {239 return Object.keys(servers).map((name) => ({240 category: t('MCP Servers'),241 name,242 status: 'pass' as const,243 message: config?.isMcpServerDisabled(name)244 ? t('disabled')245 : t('configured (not checked in non-interactive mode)'),246 }));247 }248 249 return Object.keys(servers).map((name) => {250 // Skip disabled servers — report as informational pass251 if (config?.isMcpServerDisabled(name)) {252 return {253 category: t('MCP Servers'),254 name,255 status: 'pass' as const,256 message: t('disabled'),257 };258 }259 260 const status = getMCPServerStatus(name);261 switch (status) {262 case MCPServerStatus.CONNECTED:263 return {264 category: t('MCP Servers'),265 name,266 status: 'pass' as const,267 message: t('connected'),268 };269 case MCPServerStatus.CONNECTING:270 return {271 category: t('MCP Servers'),272 name,273 status: 'warn' as const,274 message: t('connecting'),275 detail: t('Server is still starting up.'),276 };277 case MCPServerStatus.DISCONNECTED:278 default:279 return {280 category: t('MCP Servers'),281 name,282 status: 'fail' as const,283 message: t('disconnected'),284 detail: t(285 'Check that the server process is running and configuration is correct.',286 ),287 };288 }289 });290}291 292function checkToolRegistry(context: CommandContext): DoctorCheckResult {293 const registry = context.services.config?.getToolRegistry();294 if (!registry) {295 return {296 category: t('Tools'),297 name: t('Tool registry'),298 status: 'fail',299 message: t('not loaded'),300 };301 }302 const count = registry.getAllTools().length;303 return {304 category: t('Tools'),305 name: t('Tool registry'),306 status: 'pass',307 message: t('{{count}} tools registered', { count: String(count) }),308 };309}310 311async function checkRipgrep(312 context: CommandContext,313): Promise<DoctorCheckResult> {314 try {315 const useBuiltin = context.services.config?.getUseBuiltinRipgrep() ?? false;316 const result = await canUseRipgrep(useBuiltin);317 if (result) {318 return {319 category: t('Tools'),320 name: t('Ripgrep'),321 status: 'pass',322 message: t('available'),323 };324 }325 return {326 category: t('Tools'),327 name: t('Ripgrep'),328 status: 'warn',329 message: t('not available'),330 detail: t(331 'Install ripgrep for faster file search: https://github.com/BurntSushi/ripgrep',332 ),333 };334 } catch {335 return {336 category: t('Tools'),337 name: t('Ripgrep'),338 status: 'warn',339 message: t('check failed'),340 };341 }342}343 344async function checkGit(_context: CommandContext): Promise<DoctorCheckResult> {345 const version = await getGitVersion();346 if (version === 'unknown') {347 return {348 category: t('Git'),349 name: t('Git'),350 status: 'warn',351 message: t('not available'),352 detail: t('Git features will be limited.'),353 };354 }355 return {356 category: t('Git'),357 name: t('Git'),358 status: 'pass',359 message: version,360 };361}362 363/**364 * Run all doctor diagnostic checks.365 */366export async function runDoctorChecks(367 context: CommandContext,368): Promise<DoctorCheckResult[]> {369 // Run async checks in parallel370 const [npmResult, ripgrepResult, apiClientResult, gitResult] =371 await Promise.all([372 checkNpmVersion(),373 checkRipgrep(context),374 checkApiClient(context),375 checkGit(context),376 ]);377 378 return [379 // System380 checkNodeVersion(),381 npmResult,382 checkPlatform(),383 // Authentication384 checkAuth(context),385 apiClientResult,386 // Configuration387 checkSettings(context),388 checkModel(context),389 // MCP Servers390 ...checkMcpServers(context),391 // Tools392 checkToolRegistry(context),393 ripgrepResult,394 // Git395 gitResult,396 ];397}398 