mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor(vscode-ide-companion/cli): consolidate CLI detection and version management
- Replace separate CliDetector, CliVersionChecker, and CliVersionManager classes with unified CliManager - Remove redundant code and simplify CLI detection logic - Maintain all existing functionality while improving code organization - Update imports in dependent files to use CliManager This change reduces complexity by consolidating CLI-related functionality into a single manager class.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js';
|
import type { CliFeatureFlags, CliVersionInfo } from './cliManager.js';
|
||||||
|
|
||||||
export class CliContextManager {
|
export class CliContextManager {
|
||||||
private static instance: CliContextManager;
|
private static instance: CliContextManager;
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
export interface CliDetectionResult {
|
|
||||||
isInstalled: boolean;
|
|
||||||
cliPath?: string;
|
|
||||||
version?: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects if Qwen Code CLI is installed and accessible
|
|
||||||
*/
|
|
||||||
export class CliDetector {
|
|
||||||
private static cachedResult: CliDetectionResult | null = null;
|
|
||||||
private static lastCheckTime: number = 0;
|
|
||||||
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight CLI Detection Method
|
|
||||||
*
|
|
||||||
* This method is designed for performance optimization, checking only if the CLI exists
|
|
||||||
* without retrieving version information.
|
|
||||||
* Suitable for quick detection scenarios, such as pre-checks before initializing connections.
|
|
||||||
*
|
|
||||||
* Compared to the full detectQwenCli method, this method:
|
|
||||||
* - Omits version information retrieval step
|
|
||||||
* - Uses shorter timeout (3 seconds)
|
|
||||||
* - Faster response time
|
|
||||||
*
|
|
||||||
* @param forceRefresh - Whether to force refresh cached results, default is false
|
|
||||||
* @returns Promise<CliDetectionResult> - Detection result containing installation status and path
|
|
||||||
*/
|
|
||||||
static async detectQwenCliLightweight(
|
|
||||||
forceRefresh = false,
|
|
||||||
): Promise<CliDetectionResult> {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Check if cached result is available and not expired (30-second validity)
|
|
||||||
if (
|
|
||||||
!forceRefresh &&
|
|
||||||
this.cachedResult &&
|
|
||||||
now - this.lastCheckTime < this.CACHE_DURATION_MS
|
|
||||||
) {
|
|
||||||
return this.cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isWindows = process.platform === 'win32';
|
|
||||||
const whichCommand = isWindows ? 'where' : 'which';
|
|
||||||
|
|
||||||
// Check if qwen command exists
|
|
||||||
try {
|
|
||||||
// Use simplified detection without NVM for speed
|
|
||||||
const detectionCommand = isWindows
|
|
||||||
? `${whichCommand} qwen`
|
|
||||||
: `${whichCommand} qwen`;
|
|
||||||
|
|
||||||
// Execute command to detect CLI path, set shorter timeout (3 seconds)
|
|
||||||
const { stdout } = await execAsync(detectionCommand, {
|
|
||||||
timeout: 3000, // Reduced timeout for faster detection
|
|
||||||
shell: isWindows ? undefined : '/bin/bash',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Output may contain multiple lines, get first line as actual path
|
|
||||||
const lines = stdout
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => line.trim());
|
|
||||||
const cliPath = lines[0]; // Take only the first path
|
|
||||||
|
|
||||||
// Build successful detection result, note no version information
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: true,
|
|
||||||
cliPath,
|
|
||||||
// Version information not retrieved in lightweight detection
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
} catch (detectionError) {
|
|
||||||
console.log('[CliDetector] CLI not found, error:', detectionError);
|
|
||||||
|
|
||||||
// CLI not found, build error message
|
|
||||||
let error = `Qwen Code CLI not found in PATH. Please install using: npm install -g @qwen-code/qwen-code@latest`;
|
|
||||||
|
|
||||||
// Provide specific guidance for permission errors
|
|
||||||
if (detectionError instanceof Error) {
|
|
||||||
const errorMessage = detectionError.message;
|
|
||||||
if (
|
|
||||||
errorMessage.includes('EACCES') ||
|
|
||||||
errorMessage.includes('Permission denied')
|
|
||||||
) {
|
|
||||||
error += `\n\nThis may be due to permission issues. Solutions:
|
|
||||||
\n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
|
||||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
|
||||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
|
||||||
\n4. Check PATH environment variable includes npm's global bin directory`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: false,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[CliDetector] General detection error:', error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`;
|
|
||||||
|
|
||||||
// Provide specific guidance for permission errors
|
|
||||||
if (
|
|
||||||
errorMessage.includes('EACCES') ||
|
|
||||||
errorMessage.includes('Permission denied')
|
|
||||||
) {
|
|
||||||
userFriendlyError += `\n\nThis may be due to permission issues. Solutions:
|
|
||||||
\n1. Reinstall CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
|
||||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
|
||||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
|
||||||
\n4. Check PATH environment variable includes npm's global bin directory`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: false,
|
|
||||||
error: userFriendlyError,
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the Qwen Code CLI is installed
|
|
||||||
* @param forceRefresh - Force a new check, ignoring cache
|
|
||||||
* @returns Detection result with installation status and details
|
|
||||||
*/
|
|
||||||
static async detectQwenCli(
|
|
||||||
forceRefresh = false,
|
|
||||||
): Promise<CliDetectionResult> {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Return cached result if available and not expired
|
|
||||||
if (
|
|
||||||
!forceRefresh &&
|
|
||||||
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 {
|
|
||||||
// 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: isWindows ? undefined : '/bin/bash',
|
|
||||||
});
|
|
||||||
// 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 {
|
|
||||||
// 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: isWindows ? undefined : '/bin/bash',
|
|
||||||
});
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: true,
|
|
||||||
cliPath,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
} catch (detectionError) {
|
|
||||||
console.log('[CliDetector] CLI not found, error:', detectionError);
|
|
||||||
// CLI not found
|
|
||||||
let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`;
|
|
||||||
|
|
||||||
// Provide specific guidance for permission errors
|
|
||||||
if (detectionError instanceof Error) {
|
|
||||||
const errorMessage = detectionError.message;
|
|
||||||
if (
|
|
||||||
errorMessage.includes('EACCES') ||
|
|
||||||
errorMessage.includes('Permission denied')
|
|
||||||
) {
|
|
||||||
error += `\n\nThis may be due to permission issues. Possible solutions:
|
|
||||||
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
|
||||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
|
||||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
|
||||||
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: false,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[CliDetector] General detection error:', error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`;
|
|
||||||
|
|
||||||
// Provide specific guidance for permission errors
|
|
||||||
if (
|
|
||||||
errorMessage.includes('EACCES') ||
|
|
||||||
errorMessage.includes('Permission denied')
|
|
||||||
) {
|
|
||||||
userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions:
|
|
||||||
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
|
||||||
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
|
||||||
\n3. Use nvm for Node.js version management to avoid permission issues
|
|
||||||
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedResult = {
|
|
||||||
isInstalled: false,
|
|
||||||
error: userFriendlyError,
|
|
||||||
};
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
return this.cachedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the cached detection result
|
|
||||||
*/
|
|
||||||
static clearCache(): void {
|
|
||||||
this.cachedResult = null;
|
|
||||||
this.lastCheckTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets installation instructions based on the platform
|
|
||||||
*/
|
|
||||||
static getInstallationInstructions(): {
|
|
||||||
title: string;
|
|
||||||
steps: string[];
|
|
||||||
documentationUrl: string;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
title: 'Qwen Code CLI is not installed',
|
|
||||||
steps: [
|
|
||||||
'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',
|
|
||||||
' npm install',
|
|
||||||
' npm install -g .',
|
|
||||||
'',
|
|
||||||
'After installation, reload VS Code or restart the extension.',
|
|
||||||
],
|
|
||||||
documentationUrl: 'https://github.com/QwenLM/qwen-code#installation',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CliDetector } from './cliDetector.js';
|
import { CliManager } from './cliManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI Detection and Installation Handler
|
* CLI Detection and Installation Handler
|
||||||
@@ -20,7 +20,7 @@ export class CliInstaller {
|
|||||||
sendToWebView: (message: unknown) => void,
|
sendToWebView: (message: unknown) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await CliDetector.detectQwenCli();
|
const result = await CliManager.detectQwenCli();
|
||||||
|
|
||||||
sendToWebView({
|
sendToWebView({
|
||||||
type: 'cliDetectionResult',
|
type: 'cliDetectionResult',
|
||||||
@@ -31,7 +31,7 @@ export class CliInstaller {
|
|||||||
error: result.error,
|
error: result.error,
|
||||||
installInstructions: result.isInstalled
|
installInstructions: result.isInstalled
|
||||||
? undefined
|
? undefined
|
||||||
: CliDetector.getInstallationInstructions(),
|
: CliManager.getInstallationInstructions(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,8 +134,8 @@ export class CliInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache and recheck
|
// Clear cache and recheck
|
||||||
CliDetector.clearCache();
|
CliManager.clearCache();
|
||||||
const detection = await CliDetector.detectQwenCli();
|
const detection = await CliManager.detectQwenCli();
|
||||||
|
|
||||||
if (detection.isInstalled) {
|
if (detection.isInstalled) {
|
||||||
vscode.window
|
vscode.window
|
||||||
|
|||||||
498
packages/vscode-ide-companion/src/cli/cliManager.ts
Normal file
498
packages/vscode-ide-companion/src/cli/cliManager.ts
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import semver from 'semver';
|
||||||
|
import { CliInstaller } from './cliInstaller.js';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0';
|
||||||
|
|
||||||
|
export interface CliDetectionResult {
|
||||||
|
isInstalled: boolean;
|
||||||
|
cliPath?: string;
|
||||||
|
version?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CliFeatureFlags {
|
||||||
|
supportsSessionList: boolean;
|
||||||
|
supportsSessionLoad: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CliVersionInfo {
|
||||||
|
version: string | undefined;
|
||||||
|
isSupported: boolean;
|
||||||
|
features: CliFeatureFlags;
|
||||||
|
detectionResult: CliDetectionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CliManager {
|
||||||
|
private static instance: CliManager;
|
||||||
|
private lastNotificationTime: number = 0;
|
||||||
|
private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown
|
||||||
|
private context: vscode.ExtensionContext | undefined;
|
||||||
|
|
||||||
|
// Cache mechanisms
|
||||||
|
private static cachedDetectionResult: CliDetectionResult | null = null;
|
||||||
|
private static detectionLastCheckTime: number = 0;
|
||||||
|
private cachedVersionInfo: CliVersionInfo | null = null;
|
||||||
|
private versionLastCheckTime: number = 0;
|
||||||
|
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
|
||||||
|
|
||||||
|
private constructor(context?: vscode.ExtensionContext) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*/
|
||||||
|
static getInstance(context?: vscode.ExtensionContext): CliManager {
|
||||||
|
if (!CliManager.instance && context) {
|
||||||
|
CliManager.instance = new CliManager(context);
|
||||||
|
}
|
||||||
|
return CliManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the Qwen Code CLI is installed
|
||||||
|
* @param forceRefresh - Force a new check, ignoring cache
|
||||||
|
* @returns Detection result with installation status and details
|
||||||
|
*/
|
||||||
|
static async detectQwenCli(
|
||||||
|
forceRefresh = false,
|
||||||
|
): Promise<CliDetectionResult> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Return cached result if available and not expired
|
||||||
|
if (
|
||||||
|
!forceRefresh &&
|
||||||
|
this.cachedDetectionResult &&
|
||||||
|
now - this.detectionLastCheckTime < this.CACHE_DURATION_MS
|
||||||
|
) {
|
||||||
|
console.log('[CliManager] Returning cached detection result');
|
||||||
|
return this.cachedDetectionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[CliManager] 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 {
|
||||||
|
// 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(
|
||||||
|
'[CliManager] Detecting CLI with command:',
|
||||||
|
detectionCommand,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(detectionCommand, {
|
||||||
|
timeout: 5000,
|
||||||
|
shell: isWindows ? undefined : '/bin/bash',
|
||||||
|
});
|
||||||
|
// 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('[CliManager] Found CLI at:', cliPath);
|
||||||
|
|
||||||
|
// Try to get version
|
||||||
|
let version: string | undefined;
|
||||||
|
try {
|
||||||
|
// 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(
|
||||||
|
'[CliManager] Getting version with command:',
|
||||||
|
versionCommand,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { stdout: versionOutput } = await execAsync(versionCommand, {
|
||||||
|
timeout: 5000,
|
||||||
|
shell: isWindows ? undefined : '/bin/bash',
|
||||||
|
});
|
||||||
|
// 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('[CliManager] CLI version:', version);
|
||||||
|
} catch (versionError) {
|
||||||
|
console.log('[CliManager] Failed to get CLI version:', versionError);
|
||||||
|
// Version check failed, but CLI is installed
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedDetectionResult = {
|
||||||
|
isInstalled: true,
|
||||||
|
cliPath,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
this.detectionLastCheckTime = now;
|
||||||
|
return this.cachedDetectionResult;
|
||||||
|
} catch (detectionError) {
|
||||||
|
console.log('[CliManager] CLI not found, error:', detectionError);
|
||||||
|
// CLI not found
|
||||||
|
let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`;
|
||||||
|
|
||||||
|
// Provide specific guidance for permission errors
|
||||||
|
if (detectionError instanceof Error) {
|
||||||
|
const errorMessage = detectionError.message;
|
||||||
|
if (
|
||||||
|
errorMessage.includes('EACCES') ||
|
||||||
|
errorMessage.includes('Permission denied')
|
||||||
|
) {
|
||||||
|
error += `\n\nThis may be due to permission issues. Possible solutions:
|
||||||
|
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
||||||
|
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
||||||
|
\n3. Use nvm for Node.js version management to avoid permission issues
|
||||||
|
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedDetectionResult = {
|
||||||
|
isInstalled: false,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
this.detectionLastCheckTime = now;
|
||||||
|
return this.cachedDetectionResult;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[CliManager] General detection error:', error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`;
|
||||||
|
|
||||||
|
// Provide specific guidance for permission errors
|
||||||
|
if (
|
||||||
|
errorMessage.includes('EACCES') ||
|
||||||
|
errorMessage.includes('Permission denied')
|
||||||
|
) {
|
||||||
|
userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions:
|
||||||
|
\n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest
|
||||||
|
\n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code
|
||||||
|
\n3. Use nvm for Node.js version management to avoid permission issues
|
||||||
|
\n4. Check your PATH environment variable includes npm's global bin directory`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedDetectionResult = {
|
||||||
|
isInstalled: false,
|
||||||
|
error: userFriendlyError,
|
||||||
|
};
|
||||||
|
this.detectionLastCheckTime = now;
|
||||||
|
return this.cachedDetectionResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the cached detection result
|
||||||
|
*/
|
||||||
|
static clearCache(): void {
|
||||||
|
this.cachedDetectionResult = null;
|
||||||
|
this.detectionLastCheckTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets installation instructions based on the platform
|
||||||
|
*/
|
||||||
|
static getInstallationInstructions(): {
|
||||||
|
title: string;
|
||||||
|
steps: string[];
|
||||||
|
documentationUrl: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
title: 'Qwen Code CLI is not installed',
|
||||||
|
steps: [
|
||||||
|
'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',
|
||||||
|
' npm install',
|
||||||
|
' npm install -g .',
|
||||||
|
'',
|
||||||
|
'After installation, reload VS Code or restart the extension.',
|
||||||
|
],
|
||||||
|
documentationUrl: 'https://github.com/QwenLM/qwen-code#installation',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CLI version meets minimum requirements
|
||||||
|
*
|
||||||
|
* @param version - Version string to check
|
||||||
|
* @param minVersion - Minimum required version
|
||||||
|
* @returns Whether version meets requirements
|
||||||
|
*/
|
||||||
|
private isVersionSupported(
|
||||||
|
version: string | undefined,
|
||||||
|
minVersion: string,
|
||||||
|
): boolean {
|
||||||
|
if (!version) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use semver for robust comparison (handles v-prefix, pre-release, etc.)
|
||||||
|
const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null;
|
||||||
|
const min =
|
||||||
|
semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null;
|
||||||
|
|
||||||
|
if (!v || !min) {
|
||||||
|
console.warn(
|
||||||
|
`[CliManager] Invalid semver: version=${version}, min=${minVersion}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(`[CliManager] Version ${v} meets requirements: ${min}`);
|
||||||
|
return semver.gte(v, min);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get feature flags based on CLI version
|
||||||
|
*
|
||||||
|
* @param version - CLI version string
|
||||||
|
* @returns Feature flags
|
||||||
|
*/
|
||||||
|
private getFeatureFlags(version: string | undefined): CliFeatureFlags {
|
||||||
|
const isSupportedVersion = this.isVersionSupported(
|
||||||
|
version,
|
||||||
|
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
supportsSessionList: isSupportedVersion,
|
||||||
|
supportsSessionLoad: isSupportedVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect CLI version and features
|
||||||
|
*
|
||||||
|
* @param forceRefresh - Force a new check, ignoring cache
|
||||||
|
* @returns CLI version information
|
||||||
|
*/
|
||||||
|
async detectCliVersion(forceRefresh = false): Promise<CliVersionInfo> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Return cached result if available and not expired
|
||||||
|
if (
|
||||||
|
!forceRefresh &&
|
||||||
|
this.cachedVersionInfo &&
|
||||||
|
now - this.versionLastCheckTime < CliManager.CACHE_DURATION_MS
|
||||||
|
) {
|
||||||
|
console.log('[CliManager] Returning cached version info');
|
||||||
|
return this.cachedVersionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[CliManager] Detecting CLI version...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Detect CLI installation
|
||||||
|
const detectionResult = await CliManager.detectQwenCli(forceRefresh);
|
||||||
|
|
||||||
|
const versionInfo: CliVersionInfo = {
|
||||||
|
version: detectionResult.version,
|
||||||
|
isSupported: this.isVersionSupported(
|
||||||
|
detectionResult.version,
|
||||||
|
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
||||||
|
),
|
||||||
|
features: this.getFeatureFlags(detectionResult.version),
|
||||||
|
detectionResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this.cachedVersionInfo = versionInfo;
|
||||||
|
this.versionLastCheckTime = now;
|
||||||
|
|
||||||
|
console.log('[CliManager] CLI version detection result:', versionInfo);
|
||||||
|
|
||||||
|
return versionInfo;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[CliManager] Failed to detect CLI version:', error);
|
||||||
|
|
||||||
|
// Return fallback result
|
||||||
|
const fallbackResult: CliVersionInfo = {
|
||||||
|
version: undefined,
|
||||||
|
isSupported: false,
|
||||||
|
features: {
|
||||||
|
supportsSessionList: false,
|
||||||
|
supportsSessionLoad: false,
|
||||||
|
},
|
||||||
|
detectionResult: {
|
||||||
|
isInstalled: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbackResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached version information
|
||||||
|
*/
|
||||||
|
clearVersionCache(): void {
|
||||||
|
this.cachedVersionInfo = null;
|
||||||
|
this.versionLastCheckTime = 0;
|
||||||
|
CliManager.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CLI supports session/list method
|
||||||
|
*
|
||||||
|
* @param forceRefresh - Force a new check, ignoring cache
|
||||||
|
* @returns Whether session/list is supported
|
||||||
|
*/
|
||||||
|
async supportsSessionList(forceRefresh = false): Promise<boolean> {
|
||||||
|
const versionInfo = await this.detectCliVersion(forceRefresh);
|
||||||
|
return versionInfo.features.supportsSessionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CLI supports session/load method
|
||||||
|
*
|
||||||
|
* @param forceRefresh - Force a new check, ignoring cache
|
||||||
|
* @returns Whether session/load is supported
|
||||||
|
*/
|
||||||
|
async supportsSessionLoad(forceRefresh = false): Promise<boolean> {
|
||||||
|
const versionInfo = await this.detectCliVersion(forceRefresh);
|
||||||
|
return versionInfo.features.supportsSessionLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check CLI version with cooldown to prevent spamming notifications
|
||||||
|
*
|
||||||
|
* @param showNotifications - Whether to show notifications for issues
|
||||||
|
* @returns Promise with version check result
|
||||||
|
*/
|
||||||
|
async checkCliVersion(showNotifications: boolean = true): Promise<{
|
||||||
|
isInstalled: boolean;
|
||||||
|
version?: string;
|
||||||
|
isSupported: boolean;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Detect CLI installation
|
||||||
|
const detectionResult: CliDetectionResult =
|
||||||
|
await CliManager.detectQwenCli();
|
||||||
|
|
||||||
|
if (!detectionResult.isInstalled) {
|
||||||
|
if (showNotifications && this.canShowNotification()) {
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Qwen Code CLI not found. Please install it using: npm install -g @qwen-code/qwen-code@latest`,
|
||||||
|
);
|
||||||
|
this.lastNotificationTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInstalled: false,
|
||||||
|
error: detectionResult.error,
|
||||||
|
isSupported: false,
|
||||||
|
needsUpdate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version information
|
||||||
|
const versionInfo = await this.detectCliVersion();
|
||||||
|
|
||||||
|
const currentVersion = detectionResult.version;
|
||||||
|
const isSupported = versionInfo.isSupported;
|
||||||
|
|
||||||
|
// Check if update is needed (version is too old)
|
||||||
|
const needsUpdate = currentVersion
|
||||||
|
? !semver.satisfies(
|
||||||
|
currentVersion,
|
||||||
|
`>=${MIN_CLI_VERSION_FOR_SESSION_METHODS}`,
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Show notification only if needed and within cooldown period
|
||||||
|
if (showNotifications && !isSupported && this.canShowNotification()) {
|
||||||
|
vscode.window
|
||||||
|
.showWarningMessage(
|
||||||
|
`Qwen Code CLI version ${currentVersion} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later`,
|
||||||
|
'Upgrade Now',
|
||||||
|
'View Documentation',
|
||||||
|
)
|
||||||
|
.then(async (selection) => {
|
||||||
|
if (selection === 'Upgrade Now') {
|
||||||
|
await CliInstaller.install();
|
||||||
|
} else if (selection === 'View Documentation') {
|
||||||
|
vscode.env.openExternal(
|
||||||
|
vscode.Uri.parse(
|
||||||
|
'https://github.com/QwenLM/qwen-code#installation',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.lastNotificationTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInstalled: true,
|
||||||
|
version: currentVersion,
|
||||||
|
isSupported,
|
||||||
|
needsUpdate,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[CliManager] Version check failed:', error);
|
||||||
|
|
||||||
|
if (showNotifications && this.canShowNotification()) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to check Qwen Code CLI version: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
this.lastNotificationTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInstalled: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
isSupported: false,
|
||||||
|
needsUpdate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if notification can be shown based on cooldown period
|
||||||
|
*/
|
||||||
|
private canShowNotification(): boolean {
|
||||||
|
return (
|
||||||
|
Date.now() - this.lastNotificationTime >
|
||||||
|
CliManager.NOTIFICATION_COOLDOWN_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
|
|
||||||
import {
|
|
||||||
CliVersionManager,
|
|
||||||
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
|
||||||
} from './cliVersionManager.js';
|
|
||||||
import semver from 'semver';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CLI Version Checker
|
|
||||||
*
|
|
||||||
* Handles CLI version checking with throttling to prevent frequent notifications.
|
|
||||||
* This class manages version checking and provides version information without
|
|
||||||
* constantly bothering the user with popups.
|
|
||||||
*/
|
|
||||||
export class CliVersionChecker {
|
|
||||||
private static instance: CliVersionChecker;
|
|
||||||
private lastNotificationTime: number = 0;
|
|
||||||
private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown
|
|
||||||
private context: vscode.ExtensionContext;
|
|
||||||
|
|
||||||
private constructor(context: vscode.ExtensionContext) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get singleton instance
|
|
||||||
*/
|
|
||||||
static getInstance(context?: vscode.ExtensionContext): CliVersionChecker {
|
|
||||||
if (!CliVersionChecker.instance && context) {
|
|
||||||
CliVersionChecker.instance = new CliVersionChecker(context);
|
|
||||||
}
|
|
||||||
return CliVersionChecker.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check CLI version with cooldown to prevent spamming notifications
|
|
||||||
*
|
|
||||||
* @param showNotifications - Whether to show notifications for issues
|
|
||||||
* @returns Promise with version check result
|
|
||||||
*/
|
|
||||||
async checkCliVersion(showNotifications: boolean = true): Promise<{
|
|
||||||
isInstalled: boolean;
|
|
||||||
version?: string;
|
|
||||||
isSupported: boolean;
|
|
||||||
needsUpdate: boolean;
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
// Detect CLI installation
|
|
||||||
const detectionResult: CliDetectionResult =
|
|
||||||
await CliDetector.detectQwenCli();
|
|
||||||
|
|
||||||
if (!detectionResult.isInstalled) {
|
|
||||||
if (showNotifications && this.canShowNotification()) {
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
`Qwen Code CLI not found. Please install it using: npm install -g @qwen-code/qwen-code@latest`,
|
|
||||||
);
|
|
||||||
this.lastNotificationTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isInstalled: false,
|
|
||||||
error: detectionResult.error,
|
|
||||||
isSupported: false,
|
|
||||||
needsUpdate: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get version information
|
|
||||||
const versionManager = CliVersionManager.getInstance();
|
|
||||||
const versionInfo = await versionManager.detectCliVersion();
|
|
||||||
|
|
||||||
const currentVersion = detectionResult.version;
|
|
||||||
const isSupported = versionInfo.isSupported;
|
|
||||||
|
|
||||||
// Check if update is needed (version is too old)
|
|
||||||
const needsUpdate = currentVersion
|
|
||||||
? !semver.satisfies(
|
|
||||||
currentVersion,
|
|
||||||
`>=${MIN_CLI_VERSION_FOR_SESSION_METHODS}`,
|
|
||||||
)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
// Show notification only if needed and within cooldown period
|
|
||||||
if (showNotifications && !isSupported && this.canShowNotification()) {
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
`Qwen Code CLI version ${currentVersion} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later`,
|
|
||||||
);
|
|
||||||
this.lastNotificationTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isInstalled: true,
|
|
||||||
version: currentVersion,
|
|
||||||
isSupported,
|
|
||||||
needsUpdate,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[CliVersionChecker] Version check failed:', error);
|
|
||||||
|
|
||||||
if (showNotifications && this.canShowNotification()) {
|
|
||||||
vscode.window.showErrorMessage(
|
|
||||||
`Failed to check Qwen Code CLI version: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
this.lastNotificationTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isInstalled: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
isSupported: false,
|
|
||||||
needsUpdate: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if notification can be shown based on cooldown period
|
|
||||||
*/
|
|
||||||
private canShowNotification(): boolean {
|
|
||||||
return (
|
|
||||||
Date.now() - this.lastNotificationTime >
|
|
||||||
CliVersionChecker.NOTIFICATION_COOLDOWN_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import semver from 'semver';
|
|
||||||
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
|
|
||||||
|
|
||||||
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0';
|
|
||||||
|
|
||||||
export interface CliFeatureFlags {
|
|
||||||
supportsSessionList: boolean;
|
|
||||||
supportsSessionLoad: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CliVersionInfo {
|
|
||||||
version: string | undefined;
|
|
||||||
isSupported: boolean;
|
|
||||||
features: CliFeatureFlags;
|
|
||||||
detectionResult: CliDetectionResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CLI Version Manager
|
|
||||||
*
|
|
||||||
* Manages CLI version detection and feature availability based on version
|
|
||||||
*/
|
|
||||||
export class CliVersionManager {
|
|
||||||
private static instance: CliVersionManager;
|
|
||||||
private cachedVersionInfo: CliVersionInfo | null = null;
|
|
||||||
private lastCheckTime: number = 0;
|
|
||||||
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get singleton instance
|
|
||||||
*/
|
|
||||||
static getInstance(): CliVersionManager {
|
|
||||||
if (!CliVersionManager.instance) {
|
|
||||||
CliVersionManager.instance = new CliVersionManager();
|
|
||||||
}
|
|
||||||
return CliVersionManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if CLI version meets minimum requirements
|
|
||||||
*
|
|
||||||
* @param version - Version string to check
|
|
||||||
* @param minVersion - Minimum required version
|
|
||||||
* @returns Whether version meets requirements
|
|
||||||
*/
|
|
||||||
private isVersionSupported(
|
|
||||||
version: string | undefined,
|
|
||||||
minVersion: string,
|
|
||||||
): boolean {
|
|
||||||
if (!version) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use semver for robust comparison (handles v-prefix, pre-release, etc.)
|
|
||||||
const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null;
|
|
||||||
const min =
|
|
||||||
semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null;
|
|
||||||
|
|
||||||
if (!v || !min) {
|
|
||||||
console.warn(
|
|
||||||
`[CliVersionManager] Invalid semver: version=${version}, min=${minVersion}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log(`[CliVersionManager] Version ${v} meets requirements: ${min}`);
|
|
||||||
return semver.gte(v, min);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get feature flags based on CLI version
|
|
||||||
*
|
|
||||||
* @param version - CLI version string
|
|
||||||
* @returns Feature flags
|
|
||||||
*/
|
|
||||||
private getFeatureFlags(version: string | undefined): CliFeatureFlags {
|
|
||||||
const isSupportedVersion = this.isVersionSupported(
|
|
||||||
version,
|
|
||||||
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
supportsSessionList: isSupportedVersion,
|
|
||||||
supportsSessionLoad: isSupportedVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect CLI version and features
|
|
||||||
*
|
|
||||||
* @param forceRefresh - Force a new check, ignoring cache
|
|
||||||
* @returns CLI version information
|
|
||||||
*/
|
|
||||||
async detectCliVersion(forceRefresh = false): Promise<CliVersionInfo> {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Return cached result if available and not expired
|
|
||||||
if (
|
|
||||||
!forceRefresh &&
|
|
||||||
this.cachedVersionInfo &&
|
|
||||||
now - this.lastCheckTime < CliVersionManager.CACHE_DURATION_MS
|
|
||||||
) {
|
|
||||||
console.log('[CliVersionManager] Returning cached version info');
|
|
||||||
return this.cachedVersionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[CliVersionManager] Detecting CLI version...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Detect CLI installation
|
|
||||||
const detectionResult = await CliDetector.detectQwenCli(forceRefresh);
|
|
||||||
|
|
||||||
const versionInfo: CliVersionInfo = {
|
|
||||||
version: detectionResult.version,
|
|
||||||
isSupported: this.isVersionSupported(
|
|
||||||
detectionResult.version,
|
|
||||||
MIN_CLI_VERSION_FOR_SESSION_METHODS,
|
|
||||||
),
|
|
||||||
features: this.getFeatureFlags(detectionResult.version),
|
|
||||||
detectionResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
this.cachedVersionInfo = versionInfo;
|
|
||||||
this.lastCheckTime = now;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[CliVersionManager] CLI version detection result:',
|
|
||||||
versionInfo,
|
|
||||||
);
|
|
||||||
|
|
||||||
return versionInfo;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[CliVersionManager] Failed to detect CLI version:', error);
|
|
||||||
|
|
||||||
// Return fallback result
|
|
||||||
const fallbackResult: CliVersionInfo = {
|
|
||||||
version: undefined,
|
|
||||||
isSupported: false,
|
|
||||||
features: {
|
|
||||||
supportsSessionList: false,
|
|
||||||
supportsSessionLoad: false,
|
|
||||||
},
|
|
||||||
detectionResult: {
|
|
||||||
isInstalled: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return fallbackResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cached version information
|
|
||||||
*/
|
|
||||||
clearCache(): void {
|
|
||||||
this.cachedVersionInfo = null;
|
|
||||||
this.lastCheckTime = 0;
|
|
||||||
CliDetector.clearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if CLI supports session/list method
|
|
||||||
*
|
|
||||||
* @param forceRefresh - Force a new check, ignoring cache
|
|
||||||
* @returns Whether session/list is supported
|
|
||||||
*/
|
|
||||||
async supportsSessionList(forceRefresh = false): Promise<boolean> {
|
|
||||||
const versionInfo = await this.detectCliVersion(forceRefresh);
|
|
||||||
return versionInfo.features.supportsSessionList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if CLI supports session/load method
|
|
||||||
*
|
|
||||||
* @param forceRefresh - Force a new check, ignoring cache
|
|
||||||
* @returns Whether session/load is supported
|
|
||||||
*/
|
|
||||||
async supportsSessionLoad(forceRefresh = false): Promise<boolean> {
|
|
||||||
const versionInfo = await this.detectCliVersion(forceRefresh);
|
|
||||||
return versionInfo.features.supportsSessionLoad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,14 +43,6 @@ vi.mock('vscode', () => ({
|
|||||||
registerWebviewPanelSerializer: vi.fn(() => ({
|
registerWebviewPanelSerializer: vi.fn(() => ({
|
||||||
dispose: vi.fn(),
|
dispose: vi.fn(),
|
||||||
})),
|
})),
|
||||||
createStatusBarItem: vi.fn(() => ({
|
|
||||||
text: '',
|
|
||||||
tooltip: '',
|
|
||||||
command: '',
|
|
||||||
show: vi.fn(),
|
|
||||||
hide: vi.fn(),
|
|
||||||
dispose: vi.fn(),
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
workspaceFolders: [],
|
workspaceFolders: [],
|
||||||
@@ -66,10 +58,6 @@ vi.mock('vscode', () => ({
|
|||||||
Uri: {
|
Uri: {
|
||||||
joinPath: vi.fn(),
|
joinPath: vi.fn(),
|
||||||
},
|
},
|
||||||
StatusBarAlignment: {
|
|
||||||
Left: 1,
|
|
||||||
Right: 2,
|
|
||||||
},
|
|
||||||
ExtensionMode: {
|
ExtensionMode: {
|
||||||
Development: 1,
|
Development: 1,
|
||||||
Production: 2,
|
Production: 2,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
|
||||||
import { CliContextManager } from '../cli/cliContextManager.js';
|
import { CliContextManager } from '../cli/cliContextManager.js';
|
||||||
import { authMethod } from '../types/acpTypes.js';
|
import { authMethod } from '../types/acpTypes.js';
|
||||||
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js';
|
import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliManager.js';
|
||||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||||
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import type { AcpConnection } from './acpConnection.js';
|
import type { AcpConnection } from './acpConnection.js';
|
||||||
import type { QwenSessionReader } from '../services/qwenSessionReader.js';
|
import type { QwenSessionReader } from '../services/qwenSessionReader.js';
|
||||||
import { CliDetector } from '../cli/cliDetector.js';
|
import { CliManager } from '../cli/cliManager.js';
|
||||||
import { authMethod } from '../types/acpTypes.js';
|
import { authMethod } from '../types/acpTypes.js';
|
||||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export class QwenConnectionHandler {
|
|||||||
let requiresAuth = false;
|
let requiresAuth = false;
|
||||||
|
|
||||||
// Check if CLI exists using standard detection (with cached results for better performance)
|
// Check if CLI exists using standard detection (with cached results for better performance)
|
||||||
const detectionResult = await CliDetector.detectQwenCli(
|
const detectionResult = await CliManager.detectQwenCli(
|
||||||
/* forceRefresh */ false, // Use cached results when available for better performance
|
/* forceRefresh */ false, // Use cached results when available for better performance
|
||||||
);
|
);
|
||||||
if (!detectionResult.isInstalled) {
|
if (!detectionResult.isInstalled) {
|
||||||
|
|||||||
@@ -4,15 +4,6 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication Error Utility
|
|
||||||
*
|
|
||||||
* Used to uniformly identify and handle various authentication-related error messages.
|
|
||||||
* Determines if re-authentication is needed by matching predefined error patterns.
|
|
||||||
*
|
|
||||||
* @param error - The error object or string to check
|
|
||||||
* @returns true if it's an authentication-related error, false otherwise
|
|
||||||
*/
|
|
||||||
const AUTH_ERROR_PATTERNS = [
|
const AUTH_ERROR_PATTERNS = [
|
||||||
'Authentication required', // Standard authentication request message
|
'Authentication required', // Standard authentication request message
|
||||||
'(code: -32000)', // RPC error code -32000 indicates authentication failure
|
'(code: -32000)', // RPC error code -32000 indicates authentication failure
|
||||||
@@ -23,14 +14,6 @@ const AUTH_ERROR_PATTERNS = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the given error is authentication-related
|
* Determines if the given error is authentication-related
|
||||||
*
|
|
||||||
* This function detects various forms of authentication errors, including:
|
|
||||||
* - Direct error objects
|
|
||||||
* - String-form error messages
|
|
||||||
* - Other types of errors converted to strings for pattern matching
|
|
||||||
*
|
|
||||||
* @param error - The error object to check, can be an Error instance, string, or other type
|
|
||||||
* @returns boolean - true if the error is authentication-related, false otherwise
|
|
||||||
*/
|
*/
|
||||||
export const isAuthenticationRequiredError = (error: unknown): boolean => {
|
export const isAuthenticationRequiredError = (error: unknown): boolean => {
|
||||||
// Null check to avoid unnecessary processing
|
// Null check to avoid unnecessary processing
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import type { AuthenticateUpdateNotification } from '../types/acpTypes.js';
|
import type { AuthenticateUpdateNotification } from '../types/acpTypes.js';
|
||||||
|
|
||||||
// Store reference to the authentication notification to allow auto-closing
|
// Store reference to the current notification
|
||||||
let authNotificationDisposable: { dispose: () => void } | null = null;
|
let currentNotification: Thenable<string | undefined> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle authentication update notifications by showing a VS Code notification
|
* Handle authentication update notifications by showing a VS Code notification
|
||||||
* with the authentication URI and a copy button.
|
* with the authentication URI and action buttons.
|
||||||
*
|
*
|
||||||
* @param data - Authentication update notification data containing the auth URI
|
* @param data - Authentication update notification data containing the auth URI
|
||||||
*/
|
*/
|
||||||
@@ -21,30 +21,21 @@ export function handleAuthenticateUpdate(
|
|||||||
): void {
|
): void {
|
||||||
const authUri = data._meta.authUri;
|
const authUri = data._meta.authUri;
|
||||||
|
|
||||||
// Dismiss any existing authentication notification
|
// Store reference to the current notification
|
||||||
if (authNotificationDisposable) {
|
currentNotification = vscode.window.showInformationMessage(
|
||||||
authNotificationDisposable.dispose();
|
`Qwen Code needs authentication. Click an action below:`,
|
||||||
authNotificationDisposable = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show an information message with the auth URI and copy button
|
|
||||||
const notificationPromise = vscode.window.showInformationMessage(
|
|
||||||
`Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`,
|
|
||||||
'Open in Browser',
|
'Open in Browser',
|
||||||
'Copy Link',
|
'Copy Link',
|
||||||
|
'Dismiss',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a simple disposable object
|
currentNotification.then((selection) => {
|
||||||
authNotificationDisposable = {
|
|
||||||
dispose: () => {
|
|
||||||
// We can't actually cancel the promise, but we can clear our reference
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
notificationPromise.then((selection) => {
|
|
||||||
if (selection === 'Open in Browser') {
|
if (selection === 'Open in Browser') {
|
||||||
// Open the authentication URI in the default browser
|
// Open the authentication URI in the default browser
|
||||||
vscode.env.openExternal(vscode.Uri.parse(authUri));
|
vscode.env.openExternal(vscode.Uri.parse(authUri));
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
'Opening authentication page in your browser...',
|
||||||
|
);
|
||||||
} else if (selection === 'Copy Link') {
|
} else if (selection === 'Copy Link') {
|
||||||
// Copy the authentication URI to clipboard
|
// Copy the authentication URI to clipboard
|
||||||
vscode.env.clipboard.writeText(authUri);
|
vscode.env.clipboard.writeText(authUri);
|
||||||
@@ -54,6 +45,6 @@ export function handleAuthenticateUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear the notification reference after user interaction
|
// Clear the notification reference after user interaction
|
||||||
authNotificationDisposable = null;
|
currentNotification = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import * as vscode from 'vscode';
|
|||||||
import { QwenAgentManager } from '../services/qwenAgentManager.js';
|
import { QwenAgentManager } from '../services/qwenAgentManager.js';
|
||||||
import { ConversationStore } from '../services/conversationStore.js';
|
import { ConversationStore } from '../services/conversationStore.js';
|
||||||
import type { AcpPermissionRequest } from '../types/acpTypes.js';
|
import type { AcpPermissionRequest } from '../types/acpTypes.js';
|
||||||
import { CliDetector } from '../cli/cliDetector.js';
|
import { CliManager } from '../cli/cliManager.js';
|
||||||
import { PanelManager } from '../webview/PanelManager.js';
|
import { PanelManager } from '../webview/PanelManager.js';
|
||||||
import { MessageHandler } from '../webview/MessageHandler.js';
|
import { MessageHandler } from '../webview/MessageHandler.js';
|
||||||
import { WebViewContent } from '../webview/WebViewContent.js';
|
import { WebViewContent } from '../webview/WebViewContent.js';
|
||||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||||
import { CliVersionChecker } from '../cli/cliVersionChecker.js';
|
|
||||||
import { getFileName } from './utils/webviewUtils.js';
|
import { getFileName } from './utils/webviewUtils.js';
|
||||||
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||||
@@ -566,7 +565,7 @@ export class WebViewProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if CLI is installed before attempting to connect
|
// Check if CLI is installed before attempting to connect
|
||||||
const cliDetection = await CliDetector.detectQwenCli();
|
const cliDetection = await CliManager.detectQwenCli();
|
||||||
|
|
||||||
if (!cliDetection.isInstalled) {
|
if (!cliDetection.isInstalled) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -590,7 +589,7 @@ export class WebViewProvider {
|
|||||||
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
console.log('[WebViewProvider] CLI version:', cliDetection.version);
|
||||||
|
|
||||||
// Perform version check with throttled notifications
|
// Perform version check with throttled notifications
|
||||||
const versionChecker = CliVersionChecker.getInstance(this.context);
|
const versionChecker = CliManager.getInstance(this.context);
|
||||||
await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam
|
await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -674,7 +673,6 @@ export class WebViewProvider {
|
|||||||
return vscode.window.withProgress(
|
return vscode.window.withProgress(
|
||||||
{
|
{
|
||||||
location: vscode.ProgressLocation.Notification,
|
location: vscode.ProgressLocation.Notification,
|
||||||
title: 'Logging in to Qwen Code... ',
|
|
||||||
cancellable: false,
|
cancellable: false,
|
||||||
},
|
},
|
||||||
async (progress) => {
|
async (progress) => {
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
|||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MessageContent content={content} onFileClick={onFileClick} />
|
<MessageContent
|
||||||
|
content={content}
|
||||||
|
onFileClick={onFileClick}
|
||||||
|
enableFileLinks={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user