basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import process from 'node:process';8import os from 'node:os';9import { execFile } from 'node:child_process';10import type { CommandContext } from '../ui/commands/types.js';11import { getCliVersion } from './version.js';12import {13 IdeClient,14 AuthType,15 createDebugLogger,16 type LspStatusSnapshot,17} from '@qwen-code/qwen-code-core';18import { formatMemoryUsage } from '../ui/utils/formatters.js';19import { GIT_COMMIT_INFO } from '../generated/git-commit.js';20 21const debugLogger = createDebugLogger('STATUS');22 23/**24 * The subset of {@link CommandContext} these helpers actually read: only25 * `services.config` and `services.settings`. Narrowing the parameter to this26 * shape lets call sites that don't have a full `CommandContext` (e.g. the27 * Settings dialog, which holds `config` + `settings` as props) pass a plain28 * object without an unsafe cast. A full `CommandContext` is still assignable.29 */30type SystemInfoContext = {31 services: Partial<Pick<CommandContext['services'], 'config' | 'settings'>>;32};33 34/**35 * System information interface containing all system-related details36 * that can be collected for debugging and reporting purposes.37 */38export interface SystemInfo {39 cliVersion: string;40 osPlatform: string;41 osArch: string;42 osRelease: string;43 nodeVersion: string;44 npmVersion: string;45 sandboxEnv: string;46 modelVersion: string;47 selectedAuthType: string;48 ideClient: string;49 sessionId: string;50 proxy?: string;51}52 53/**54 * Additional system information for bug reports55 */56export interface ExtendedSystemInfo extends SystemInfo {57 memoryUsage: string;58 baseUrl?: string;59 apiKeyEnvKey?: string;60 gitCommit?: string;61 proxy?: string;62 fastModel?: string;63 lspStatus?: string;64}65 66// `execFile` (not the shell-spawning `exec`) so a hostile binary on PATH67// can't inject shell metacharacters. The timeout protects the daemon's68// event loop from a hung `git` / `npm` (NFS stall, Gatekeeper prompt,69// broken install) — `execSync` would have blocked indefinitely.70const VERSION_PROBE_TIMEOUT_MS = 5_000;71 72/**73 * Run a tiny `<binary> --version` probe with a hard timeout, return stdout74 * trimmed, or `'unknown'` on any failure (including timeout). Helper kept75 * inline (rather than `const probeVersion = promisify(execFile)`) so a76 * `vi.mock('node:child_process', { execFile: vi.fn() })` test can override77 * each call individually — the promisified value would otherwise capture78 * the original `execFile` reference at module load.79 */80function probeVersion(binary: string): Promise<string> {81 return new Promise<string>((resolve) => {82 execFile(83 binary,84 ['--version'],85 { timeout: VERSION_PROBE_TIMEOUT_MS, encoding: 'utf-8' },86 (err, stdout) => {87 if (err) {88 resolve('unknown');89 return;90 }91 resolve(typeof stdout === 'string' ? stdout.trim() : 'unknown');92 },93 );94 });95}96 97/**98 * Gets the NPM version, handling cases where npm might not be available.99 * Returns 'unknown' if npm command fails, is not found, or exceeds the100 * version-probe timeout.101 */102export async function getNpmVersion(): Promise<string> {103 return probeVersion('npm');104}105 106/**107 * Gets the Git version, handling cases where git might not be available.108 * Returns 'unknown' if git command fails, is not found, or exceeds the109 * version-probe timeout.110 */111export async function getGitVersion(): Promise<string> {112 return probeVersion('git');113}114 115/**116 * Gets the IDE client name if IDE mode is enabled.117 * Returns empty string if IDE mode is disabled or IDE client is not detected.118 */119export async function getIdeClientName(120 context: SystemInfoContext,121): Promise<string> {122 if (!context.services.config?.getIdeMode()) {123 return '';124 }125 try {126 const ideClient = await IdeClient.getInstance();127 return ideClient?.getDetectedIdeDisplayName() ?? '';128 } catch {129 return '';130 }131}132 133/**134 * Gets the sandbox environment information.135 * Handles different sandbox types including sandbox-exec and custom sandbox environments.136 * For bug reports, removes 'qwen-' or 'qwen-code-' prefixes from sandbox names.137 *138 * @param stripPrefix - Whether to strip 'qwen-' prefix (used for bug reports)139 */140export function getSandboxEnv(stripPrefix = false): string {141 const sandbox = process.env['SANDBOX'];142 143 if (!sandbox || sandbox === 'sandbox-exec') {144 if (sandbox === 'sandbox-exec') {145 const profile = process.env['SEATBELT_PROFILE'] || 'unknown';146 return `sandbox-exec (${profile})`;147 }148 return 'no sandbox';149 }150 151 // For bug reports, remove qwen- prefix152 if (stripPrefix) {153 return sandbox.replace(/^qwen-(?:code-)?/, '');154 }155 156 return sandbox;157}158 159/**160 * Collects comprehensive system information for debugging and reporting.161 * This function gathers all system-related details including OS, versions,162 * sandbox environment, authentication, and session information.163 *164 * @param context - Command context containing config and settings165 * @returns Promise resolving to SystemInfo object with all collected information166 */167export async function getSystemInfo(168 context: SystemInfoContext,169): Promise<SystemInfo> {170 const osPlatform = process.platform;171 const osArch = process.arch;172 const osRelease = os.release();173 const nodeVersion = process.version;174 const npmVersion = await getNpmVersion();175 const sandboxEnv = getSandboxEnv();176 const modelVersion = context.services.config?.getModel() || 'Unknown';177 const cliVersion = await getCliVersion();178 const selectedAuthType = context.services.config?.getAuthType() || '';179 const ideClient = await getIdeClientName(context);180 const sessionId = context.services.config?.getSessionId() || 'unknown';181 const proxy = context.services.config?.getProxy();182 183 return {184 cliVersion,185 osPlatform,186 osArch,187 osRelease,188 nodeVersion,189 npmVersion,190 sandboxEnv,191 modelVersion,192 selectedAuthType,193 ideClient,194 sessionId,195 proxy,196 };197}198 199/**200 * Collects extended system information for bug reports.201 * Includes all standard system info plus memory usage and optional base URL.202 *203 * @param context - Command context containing config and settings204 * @returns Promise resolving to ExtendedSystemInfo object205 */206export async function getExtendedSystemInfo(207 context: SystemInfoContext,208): Promise<ExtendedSystemInfo> {209 const baseInfo = await getSystemInfo(context);210 const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);211 212 // For bug reports, use sandbox name without prefix213 const sandboxEnv = getSandboxEnv(true);214 215 // Get base URL and apiKeyEnvKey if using OpenAI or Anthropic auth216 const contentGeneratorConfig =217 baseInfo.selectedAuthType === AuthType.USE_OPENAI ||218 baseInfo.selectedAuthType === AuthType.USE_ANTHROPIC219 ? context.services.config?.getContentGeneratorConfig()220 : undefined;221 const baseUrl = contentGeneratorConfig?.baseUrl;222 const apiKeyEnvKey = contentGeneratorConfig?.apiKeyEnvKey;223 224 // Get git commit info225 const gitCommit =226 GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)227 ? GIT_COMMIT_INFO228 : undefined;229 230 // Get fast model from settings231 const fastModel = context.services.settings?.merged?.fastModel || undefined;232 const lspStatus = getLspStatus(context);233 234 return {235 ...baseInfo,236 sandboxEnv,237 memoryUsage,238 baseUrl,239 apiKeyEnvKey,240 gitCommit,241 fastModel,242 lspStatus,243 };244}245 246function getLspStatus(context: SystemInfoContext): string | undefined {247 try {248 const snapshot = context.services.config?.getLspStatusSnapshot?.();249 if (!snapshot) {250 return undefined;251 }252 253 if (context.services.config?.getDebugMode?.()) {254 debugLogger.debug('LSP status snapshot for /status:', snapshot);255 }256 257 return formatLspStatusSnapshot(snapshot);258 } catch (error) {259 if (context.services.config?.getDebugMode?.()) {260 debugLogger.debug(261 'Unable to read LSP status snapshot for /status:',262 error,263 );264 }265 return undefined;266 }267}268 269function formatLspStatusSnapshot(snapshot: LspStatusSnapshot): string {270 if (!snapshot.enabled) {271 return 'disabled';272 }273 274 if (snapshot.initializationError) {275 return `enabled, initialization failed: ${snapshot.initializationError}`;276 }277 278 if (snapshot.statusUnavailable) {279 return 'enabled, status unavailable';280 }281 282 if (snapshot.configuredServers === 0) {283 return 'enabled, no servers configured';284 }285 286 const details = [287 snapshot.failedServers > 0 ? `${snapshot.failedServers} failed` : '',288 snapshot.inProgressServers > 0289 ? `${snapshot.inProgressServers} starting`290 : '',291 snapshot.notStartedServers > 0292 ? `${snapshot.notStartedServers} not started`293 : '',294 ].filter(Boolean);295 296 const detailText = details.length > 0 ? ` (${details.join(', ')})` : '';297 return `enabled, ${snapshot.readyServers}/${snapshot.configuredServers} ready${detailText}`;298}299 