diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index 13639d4d..cc62a7aa 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -20,6 +20,7 @@ import type { } from './connectionTypes.js'; import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpSessionManager } from './acpSessionManager.js'; +import { statSync } from 'fs'; /** * ACP Connection Handler for VSCode Extension @@ -69,7 +70,75 @@ export class AcpConnection { } /** - * Connect to ACP backend + * Determine the correct Node.js executable path for a given CLI installation + * Handles various Node.js version managers (nvm, n, manual installations) + * + * @param cliPath - Path to the CLI executable + * @returns Path to the Node.js executable, or null if not found + */ + private determineNodePathForCli(cliPath: string): string | null { + // Common patterns for Node.js installations + const nodePathPatterns = [ + // NVM pattern: /Users/user/.nvm/versions/node/vXX.XX.X/bin/qwen -> /Users/user/.nvm/versions/node/vXX.XX.X/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + + // N pattern: /Users/user/n/bin/qwen -> /Users/user/n/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + + // Manual installation pattern: /usr/local/bin/qwen -> /usr/local/bin/node + cliPath.replace(/\/qwen$/, '/node'), + + // Alternative pattern: /opt/nodejs/bin/qwen -> /opt/nodejs/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + ]; + + // Check each pattern + for (const nodePath of nodePathPatterns) { + try { + if (statSync(nodePath).isFile()) { + // Verify it's executable + const stats = statSync(nodePath); + if (stats.mode & 0o111) { + // Check if executable + console.log( + `[ACP] Found Node.js executable for CLI at: ${nodePath}`, + ); + return nodePath; + } + } + } catch (_error) { + // File doesn't exist or other error, continue to next pattern + continue; + } + } + + // Try to find node in the same directory as the CLI + const cliDir = cliPath.substring(0, cliPath.lastIndexOf('/')); + const potentialNodePaths = [`${cliDir}/node`, `${cliDir}/bin/node`]; + + for (const nodePath of potentialNodePaths) { + try { + if (statSync(nodePath).isFile()) { + const stats = statSync(nodePath); + if (stats.mode & 0o111) { + console.log( + `[ACP] Found Node.js executable in CLI directory at: ${nodePath}`, + ); + return nodePath; + } + } + } catch (_error) { + // File doesn't exist, continue + continue; + } + } + + console.log(`[ACP] Could not determine Node.js path for CLI: ${cliPath}`); + return null; + } + + /** + * 连接到ACP后端 * * @param backend - Backend type * @param cliPath - CLI path @@ -115,8 +184,23 @@ export class AcpConnection { spawnCommand = isWindows ? 'npx.cmd' : 'npx'; spawnArgs = [...parts.slice(1), '--experimental-acp', ...extraArgs]; } else { - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', ...extraArgs]; + // For qwen CLI, ensure we use the correct Node.js version + // Handle various Node.js version managers (nvm, n, manual installations) + if (cliPath.includes('/qwen') && !isWindows) { + // Try to determine the correct node executable for this qwen installation + const nodePath = this.determineNodePathForCli(cliPath); + if (nodePath) { + spawnCommand = nodePath; + spawnArgs = [cliPath, '--experimental-acp', ...extraArgs]; + } else { + // Fallback to direct execution + spawnCommand = cliPath; + spawnArgs = ['--experimental-acp', ...extraArgs]; + } + } else { + spawnCommand = cliPath; + spawnArgs = ['--experimental-acp', ...extraArgs]; + } } console.log('[ACP] Spawning command:', spawnCommand, spawnArgs.join(' ')); diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 13328b51..125749c8 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -72,12 +72,14 @@ export class QwenAgentManager { /** * Connect to Qwen service * - * @param workingDir - Working directory - * @param authStateManager - Auth state manager (optional) + * @param workingDir - 工作目录 + * @param authStateManager - 认证状态管理器(可选) + * @param cliPath - CLI路径(可选,如果提供将覆盖配置中的路径) */ async connect( workingDir: string, authStateManager?: AuthStateManager, + cliPath?: string, ): Promise { this.currentWorkingDir = workingDir; await this.connectionHandler.connect( @@ -85,6 +87,7 @@ export class QwenAgentManager { this.sessionReader, workingDir, authStateManager, + cliPath, ); } diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index fce12e16..49e3eb4c 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -23,16 +23,18 @@ export class QwenConnectionHandler { /** * Connect to Qwen service and establish session * - * @param connection - ACP connection instance - * @param sessionReader - Session reader instance - * @param workingDir - Working directory - * @param authStateManager - Auth state manager (optional) + * @param connection - ACP连接实例 + * @param sessionReader - 会话读取器实例 + * @param workingDir - 工作目录 + * @param authStateManager - 认证状态管理器(可选) + * @param cliPath - CLI路径(可选,如果提供将覆盖配置中的路径) */ async connect( connection: AcpConnection, sessionReader: QwenSessionReader, workingDir: string, authStateManager?: AuthStateManager, + cliPath?: string, ): Promise { const connectId = Date.now(); console.log(`\n========================================`); @@ -41,7 +43,9 @@ export class QwenConnectionHandler { console.log(`========================================\n`); const config = vscode.workspace.getConfiguration('qwenCode'); - const cliPath = config.get('qwen.cliPath', 'qwen'); + // Use the provided CLI path if available, otherwise use the configured path + const effectiveCliPath = + cliPath || config.get('qwen.cliPath', 'qwen'); const openaiApiKey = config.get('qwen.openaiApiKey', ''); const openaiBaseUrl = config.get('qwen.openaiBaseUrl', ''); const model = config.get('qwen.model', ''); @@ -63,7 +67,7 @@ export class QwenConnectionHandler { console.log('[QwenAgentManager] Using proxy:', proxy); } - await connection.connect('qwen', cliPath, workingDir, extraArgs); + await connection.connect('qwen', effectiveCliPath, workingDir, extraArgs); // Determine authentication method const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; diff --git a/packages/vscode-ide-companion/src/utils/CliInstaller.ts b/packages/vscode-ide-companion/src/utils/CliInstaller.ts index b9aeb748..4a7da08e 100644 --- a/packages/vscode-ide-companion/src/utils/CliInstaller.ts +++ b/packages/vscode-ide-companion/src/utils/CliInstaller.ts @@ -93,9 +93,39 @@ export class CliInstaller { const execAsync = promisify(exec); try { + // Use NVM environment to ensure we get the same Node.js version + // as when they run 'node -v' in terminal + // Fallback chain: default alias -> node alias -> current version + const installCommand = + process.platform === 'win32' + ? 'npm install -g @qwen-code/qwen-code@latest' + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); npm install -g @qwen-code/qwen-code@latest'; + + console.log( + '[CliInstaller] Installing with command:', + installCommand, + ); + console.log( + '[CliInstaller] Current process PATH:', + process.env.PATH, + ); + + // Also log Node.js version being used by VS Code + console.log( + '[CliInstaller] VS Code Node.js version:', + process.version, + ); + console.log( + '[CliInstaller] VS Code Node.js execPath:', + process.execPath, + ); + const { stdout, stderr } = await execAsync( - 'npm install -g @qwen-code/qwen-code@latest', - { timeout: 120000 }, // 2 minutes timeout + installCommand, + { + timeout: 120000, + shell: '/bin/bash', + }, // 2 minutes timeout ); console.log('[CliInstaller] Installation output:', stdout); @@ -129,6 +159,7 @@ export class CliInstaller { const errorMessage = error instanceof Error ? error.message : String(error); console.error('[CliInstaller] Installation failed:', errorMessage); + console.error('[CliInstaller] Error stack:', error); vscode.window .showErrorMessage( diff --git a/packages/vscode-ide-companion/src/utils/cliDetector.ts b/packages/vscode-ide-companion/src/utils/cliDetector.ts index c09ed250..b88755dd 100644 --- a/packages/vscode-ide-companion/src/utils/cliDetector.ts +++ b/packages/vscode-ide-companion/src/utils/cliDetector.ts @@ -40,28 +40,77 @@ export class CliDetector { this.cachedResult && now - this.lastCheckTime < this.CACHE_DURATION_MS ) { + console.log('[CliDetector] Returning cached result'); return this.cachedResult; } + console.log( + '[CliDetector] Starting CLI detection, current PATH:', + process.env.PATH, + ); + try { const isWindows = process.platform === 'win32'; const whichCommand = isWindows ? 'where' : 'which'; // Check if qwen command exists try { - const { stdout } = await execAsync(`${whichCommand} qwen`, { + // Use NVM environment for consistent detection + // Fallback chain: default alias -> node alias -> current version + const detectionCommand = + process.platform === 'win32' + ? `${whichCommand} qwen` + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; + + console.log( + '[CliDetector] Detecting CLI with command:', + detectionCommand, + ); + + const { stdout } = await execAsync(detectionCommand, { timeout: 5000, + shell: '/bin/bash', }); - const cliPath = stdout.trim().split('\n')[0]; + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual path + const lines = stdout + .trim() + .split('\n') + .filter((line) => line.trim()); + const cliPath = lines[lines.length - 1]; + + console.log('[CliDetector] Found CLI at:', cliPath); // Try to get version let version: string | undefined; try { - const { stdout: versionOutput } = await execAsync('qwen --version', { + // Use NVM environment for version check + // Fallback chain: default alias -> node alias -> current version + // Also ensure we use the correct Node.js version that matches the CLI installation + const versionCommand = + process.platform === 'win32' + ? 'qwen --version' + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; + + console.log( + '[CliDetector] Getting version with command:', + versionCommand, + ); + + const { stdout: versionOutput } = await execAsync(versionCommand, { timeout: 5000, + shell: '/bin/bash', }); - version = versionOutput.trim(); - } catch { + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual version + const versionLines = versionOutput + .trim() + .split('\n') + .filter((line) => line.trim()); + version = versionLines[versionLines.length - 1]; + console.log('[CliDetector] CLI version:', version); + } catch (versionError) { + console.log('[CliDetector] Failed to get CLI version:', versionError); // Version check failed, but CLI is installed } @@ -72,7 +121,8 @@ export class CliDetector { }; this.lastCheckTime = now; return this.cachedResult; - } catch (_error) { + } catch (detectionError) { + console.log('[CliDetector] CLI not found, error:', detectionError); // CLI not found this.cachedResult = { isInstalled: false, @@ -82,6 +132,7 @@ export class CliDetector { return this.cachedResult; } } catch (error) { + console.log('[CliDetector] General detection error:', error); const errorMessage = error instanceof Error ? error.message : String(error); this.cachedResult = { @@ -115,6 +166,9 @@ export class CliDetector { 'Install via npm:', ' npm install -g @qwen-code/qwen-code@latest', '', + 'If you are using nvm (automatically handled by the plugin):', + ' The plugin will automatically use your default nvm version', + '', 'Or install from source:', ' git clone https://github.com/QwenLM/qwen-code.git', ' cd qwen-code', diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 6e924763..df6c77cd 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -399,7 +399,12 @@ export class WebViewProvider { const authInfo = await this.authStateManager.getAuthInfo(); console.log('[WebViewProvider] Auth cache status:', authInfo); - await this.agentManager.connect(workingDir, this.authStateManager); + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect( + workingDir, + this.authStateManager, + cliDetection.cliPath, + ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true;