Add integration and unit tests for CodexLens UV installation and UV manager

- Implemented integration tests for CodexLens UV installation functionality, covering package installations, Python import verification, and dependency conflict resolution.
- Created unit tests for the uv-manager utility module, including UV binary detection, installation, and virtual environment management.
- Added cleanup procedures for temporary directories used in tests.
- Verified the functionality of the UvManager class, including virtual environment creation, package installation, and error handling for invalid environments.
This commit is contained in:
catlog22
2026-01-12 12:42:38 +08:00
parent 52c510501d
commit 7803dad430
5 changed files with 1828 additions and 1 deletions

View File

@@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
import { dirname, join as pathJoin } from 'path';
import { z } from 'zod';
import { getSystemPython } from '../../utils/python-utils.js';
import {
UvManager,
isUvAvailable,
ensureUvInstalled,
createCodexLensUvManager
} from '../../utils/uv-manager.js';
import type { RouteContext } from './types.js';
// ========== Input Validation Schemas ==========
@@ -97,6 +103,47 @@ export function clearCcwLitellmStatusCache() {
ccwLitellmStatusCache.timestamp = 0;
}
/**
* Install ccw-litellm using UV package manager
* Uses CodexLens venv for consistency with other Python dependencies
* @param packagePath - Local package path, or null to install from PyPI
* @returns Installation result
*/
async function installCcwLitellmWithUv(packagePath: string | null): Promise<{ success: boolean; message?: string; error?: string }> {
try {
await ensureUvInstalled();
// Reuse CodexLens venv for consistency
const uv = createCodexLensUvManager();
// Ensure venv exists
const venvResult = await uv.createVenv();
if (!venvResult.success) {
return { success: false, error: venvResult.error };
}
if (packagePath) {
// Install from local path
const result = await uv.installFromProject(packagePath);
if (result.success) {
clearCcwLitellmStatusCache();
return { success: true, message: 'ccw-litellm installed from local path via UV' };
}
return { success: false, error: result.error };
} else {
// Install from PyPI
const result = await uv.install(['ccw-litellm']);
if (result.success) {
clearCcwLitellmStatusCache();
return { success: true, message: 'ccw-litellm installed from PyPI via UV' };
}
return { success: false, error: result.error };
}
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
function sanitizeProviderForResponse(provider: any): any {
if (!provider) return provider;
return {
@@ -1093,6 +1140,22 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
}
}
// Priority: Use UV if available
if (await isUvAvailable()) {
const uvResult = await installCcwLitellmWithUv(packagePath || null);
if (uvResult.success) {
// Broadcast installation event
broadcastToClients({
type: 'CCW_LITELLM_INSTALLED',
payload: { timestamp: new Date().toISOString(), method: 'uv' }
});
return { ...uvResult, path: packagePath || undefined };
}
// UV install failed, fall through to pip fallback
console.log('[ccw-litellm install] UV install failed, falling back to pip:', uvResult.error);
}
// Fallback: Use pip for installation
// Use shared Python detection for consistent cross-platform behavior
const pythonCmd = getSystemPython();
@@ -1108,6 +1171,10 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
if (code === 0) {
// Clear status cache after successful installation
clearCcwLitellmStatusCache();
broadcastToClients({
type: 'CCW_LITELLM_INSTALLED',
payload: { timestamp: new Date().toISOString(), method: 'pip' }
});
resolve({ success: true, message: 'ccw-litellm installed from PyPI' });
} else {
resolve({ success: false, error: error || 'Installation failed' });
@@ -1132,7 +1199,7 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
// Broadcast installation event
broadcastToClients({
type: 'CCW_LITELLM_INSTALLED',
payload: { timestamp: new Date().toISOString() }
payload: { timestamp: new Date().toISOString(), method: 'pip' }
});
resolve({ success: true, message: 'ccw-litellm installed successfully', path: packagePath });
} else {

View File

@@ -18,6 +18,12 @@ import { homedir } from 'os';
import { fileURLToPath } from 'url';
import { getSystemPython } from '../utils/python-utils.js';
import { EXEC_TIMEOUTS } from '../utils/exec-constants.js';
import {
UvManager,
ensureUvInstalled,
isUvAvailable,
createCodexLensUvManager,
} from '../utils/uv-manager.js';
// Get directory of this module
const __filename = fileURLToPath(import.meta.url);
@@ -363,6 +369,15 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
*/
type GpuMode = 'cpu' | 'cuda' | 'directml';
/**
* Mapping from GPU mode to codexlens extras for UV installation
*/
const GPU_MODE_EXTRAS: Record<GpuMode, string[]> = {
cpu: ['semantic'],
cuda: ['semantic-gpu'],
directml: ['semantic-directml'],
};
/**
* Python environment info for compatibility checks
*/
@@ -467,12 +482,165 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
return { mode: recommendedMode, available, info: detectedInfo, pythonEnv };
}
/**
* Bootstrap CodexLens venv using UV (fast package manager)
* @param gpuMode - GPU acceleration mode for semantic search
* @returns Bootstrap result
*/
async function bootstrapWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
console.log('[CodexLens] Bootstrapping with UV package manager...');
// Ensure UV is installed
const uvInstalled = await ensureUvInstalled();
if (!uvInstalled) {
return { success: false, error: 'Failed to install UV package manager' };
}
// Create UV manager for CodexLens
const uv = createCodexLensUvManager();
// Create venv if not exists
if (!uv.isVenvValid()) {
console.log('[CodexLens] Creating virtual environment with UV...');
const createResult = await uv.createVenv();
if (!createResult.success) {
return { success: false, error: `Failed to create venv: ${createResult.error}` };
}
}
// Find local codex-lens package
const possiblePaths = [
join(process.cwd(), 'codex-lens'),
join(__dirname, '..', '..', '..', 'codex-lens'), // ccw/src/tools -> project root
join(homedir(), 'codex-lens'),
];
let codexLensPath: string | null = null;
for (const localPath of possiblePaths) {
if (existsSync(join(localPath, 'pyproject.toml'))) {
codexLensPath = localPath;
break;
}
}
// Determine extras based on GPU mode
const extras = GPU_MODE_EXTRAS[gpuMode];
if (codexLensPath) {
console.log(`[CodexLens] Installing from local path with UV: ${codexLensPath}`);
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
const installResult = await uv.installFromProject(codexLensPath, extras);
if (!installResult.success) {
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
}
} else {
// Install from PyPI with extras
console.log('[CodexLens] Installing from PyPI with UV...');
const packageSpec = `codexlens[${extras.join(',')}]`;
const installResult = await uv.install([packageSpec]);
if (!installResult.success) {
return { success: false, error: `Failed to install codexlens: ${installResult.error}` };
}
}
// Clear cache after successful installation
clearVenvStatusCache();
console.log(`[CodexLens] Bootstrap with UV complete (${gpuMode} mode)`);
return { success: true, message: `Installed with UV (${gpuMode} mode)` };
}
/**
* Install semantic search dependencies using UV (fast package manager)
* UV automatically handles ONNX Runtime conflicts
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
* @returns Bootstrap result
*/
async function installSemanticWithUv(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
console.log('[CodexLens] Installing semantic dependencies with UV...');
// First check if CodexLens is installed
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
}
// Check Python environment compatibility for DirectML
if (gpuMode === 'directml') {
const pythonEnv = await checkPythonEnvForDirectML();
if (!pythonEnv.compatible) {
const errorDetails = pythonEnv.error || 'Unknown compatibility issue';
return {
success: false,
error: `DirectML installation failed: ${errorDetails}\n\nTo fix this:\n1. Uninstall current Python\n2. Install 64-bit Python 3.10, 3.11, or 3.12 from python.org\n3. Delete ~/.codexlens/venv folder\n4. Reinstall CodexLens`,
};
}
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
}
// Create UV manager
const uv = createCodexLensUvManager();
// Find local codex-lens package
const possiblePaths = [
join(process.cwd(), 'codex-lens'),
join(__dirname, '..', '..', '..', 'codex-lens'),
join(homedir(), 'codex-lens'),
];
let codexLensPath: string | null = null;
for (const localPath of possiblePaths) {
if (existsSync(join(localPath, 'pyproject.toml'))) {
codexLensPath = localPath;
break;
}
}
// Determine extras based on GPU mode
const extras = GPU_MODE_EXTRAS[gpuMode];
const modeDescription =
gpuMode === 'cuda'
? 'NVIDIA CUDA GPU acceleration'
: gpuMode === 'directml'
? 'Windows DirectML GPU acceleration'
: 'CPU (ONNX Runtime)';
console.log(`[CodexLens] Mode: ${modeDescription}`);
console.log(`[CodexLens] Extras: ${extras.join(', ')}`);
// Install with extras - UV handles dependency conflicts automatically
if (codexLensPath) {
console.log(`[CodexLens] Reinstalling from local path with semantic extras...`);
const installResult = await uv.installFromProject(codexLensPath, extras);
if (!installResult.success) {
return { success: false, error: `Installation failed: ${installResult.error}` };
}
} else {
// Install from PyPI
const packageSpec = `codexlens[${extras.join(',')}]`;
console.log(`[CodexLens] Installing ${packageSpec} from PyPI...`);
const installResult = await uv.install([packageSpec]);
if (!installResult.success) {
return { success: false, error: `Installation failed: ${installResult.error}` };
}
}
console.log(`[CodexLens] Semantic dependencies installed successfully (${gpuMode} mode)`);
return { success: true, message: `Installed with ${modeDescription}` };
}
/**
* Install semantic search dependencies with optional GPU acceleration
* @param gpuMode - GPU acceleration mode: 'cpu', 'cuda', or 'directml'
* @returns Bootstrap result
*/
async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResult> {
// Prefer UV if available
if (await isUvAvailable()) {
console.log('[CodexLens] Using UV for semantic installation...');
return installSemanticWithUv(gpuMode);
}
// Fall back to pip logic...
// First ensure CodexLens is installed
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
@@ -617,6 +785,13 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
* @returns Bootstrap result
*/
async function bootstrapVenv(): Promise<BootstrapResult> {
// Prefer UV if available (faster package resolution and installation)
if (await isUvAvailable()) {
console.log('[CodexLens] Using UV for bootstrap...');
return bootstrapWithUv();
}
// Fall back to pip logic...
// Ensure data directory exists
if (!existsSync(CODEXLENS_DATA_DIR)) {
mkdirSync(CODEXLENS_DATA_DIR, { recursive: true });
@@ -1502,6 +1677,9 @@ export {
uninstallCodexLens,
cancelIndexing,
isIndexingInProgress,
// UV-based installation functions
bootstrapWithUv,
installSemanticWithUv,
};
// Export Python path for direct spawn usage (e.g., watcher)

796
ccw/src/utils/uv-manager.ts Normal file
View File

@@ -0,0 +1,796 @@
/**
* UV Package Manager Tool
* Provides unified UV (https://github.com/astral-sh/uv) tool management capabilities
*
* Features:
* - Cross-platform UV binary discovery and installation
* - Virtual environment creation and management
* - Python dependency installation with UV's fast resolver
* - Support for local project installs with extras
*/
import { execSync, spawn } from 'child_process';
import { existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir, platform, arch } from 'os';
import { EXEC_TIMEOUTS } from './exec-constants.js';
/**
* Configuration for UvManager
*/
export interface UvManagerConfig {
/** Path to the virtual environment directory */
venvPath: string;
/** Python version requirement (e.g., ">=3.10", "3.11") */
pythonVersion?: string;
}
/**
* Result of UV operations
*/
export interface UvInstallResult {
/** Whether the operation succeeded */
success: boolean;
/** Error message if operation failed */
error?: string;
/** Duration of the operation in milliseconds */
duration?: number;
}
/**
* UV binary search locations in priority order
*/
interface UvSearchLocation {
path: string;
description: string;
}
// Platform-specific constants
const IS_WINDOWS = platform() === 'win32';
const UV_BINARY_NAME = IS_WINDOWS ? 'uv.exe' : 'uv';
const VENV_BIN_DIR = IS_WINDOWS ? 'Scripts' : 'bin';
const PYTHON_EXECUTABLE = IS_WINDOWS ? 'python.exe' : 'python';
/**
* Get the path to the UV binary
* Search order:
* 1. CCW_UV_PATH environment variable
* 2. Project vendor/uv/ directory
* 3. User local directories (~/.local/bin, ~/.cargo/bin)
* 4. System PATH
*
* @returns Path to the UV binary
*/
export function getUvBinaryPath(): string {
const searchLocations: UvSearchLocation[] = [];
// 1. Environment variable (highest priority)
const envPath = process.env.CCW_UV_PATH;
if (envPath) {
searchLocations.push({ path: envPath, description: 'CCW_UV_PATH environment variable' });
}
// 2. Project vendor directory
const vendorPaths = [
join(process.cwd(), 'vendor', 'uv', UV_BINARY_NAME),
join(dirname(process.cwd()), 'vendor', 'uv', UV_BINARY_NAME),
];
for (const vendorPath of vendorPaths) {
searchLocations.push({ path: vendorPath, description: 'Project vendor directory' });
}
// 3. User local directories
const home = homedir();
if (IS_WINDOWS) {
// Windows: AppData\Local\uv and .cargo\bin
searchLocations.push(
{ path: join(home, 'AppData', 'Local', 'uv', 'bin', UV_BINARY_NAME), description: 'UV AppData' },
{ path: join(home, '.cargo', 'bin', UV_BINARY_NAME), description: 'Cargo bin' },
{ path: join(home, '.local', 'bin', UV_BINARY_NAME), description: 'Local bin' },
);
} else {
// Unix: ~/.local/bin and ~/.cargo/bin
searchLocations.push(
{ path: join(home, '.local', 'bin', UV_BINARY_NAME), description: 'Local bin' },
{ path: join(home, '.cargo', 'bin', UV_BINARY_NAME), description: 'Cargo bin' },
);
}
// Check each location
for (const location of searchLocations) {
if (existsSync(location.path)) {
return location.path;
}
}
// 4. Try system PATH using 'which' or 'where'
try {
const cmd = IS_WINDOWS ? 'where uv' : 'which uv';
const result = execSync(cmd, { encoding: 'utf-8', timeout: EXEC_TIMEOUTS.SYSTEM_INFO, stdio: ['pipe', 'pipe', 'pipe'] });
const foundPath = result.trim().split('\n')[0];
if (foundPath && existsSync(foundPath)) {
return foundPath;
}
} catch {
// UV not found in PATH
}
// Return default path (may not exist)
if (IS_WINDOWS) {
return join(home, 'AppData', 'Local', 'uv', 'bin', UV_BINARY_NAME);
}
return join(home, '.local', 'bin', UV_BINARY_NAME);
}
/**
* Check if UV is available and working
* @returns True if UV is installed and functional
*/
export async function isUvAvailable(): Promise<boolean> {
const uvPath = getUvBinaryPath();
if (!existsSync(uvPath)) {
return false;
}
return new Promise((resolve) => {
const child = spawn(uvPath, ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PYTHON_VERSION,
});
child.on('close', (code) => {
resolve(code === 0);
});
child.on('error', () => {
resolve(false);
});
});
}
/**
* Get UV version string
* @returns UV version or null if not available
*/
export async function getUvVersion(): Promise<string | null> {
const uvPath = getUvBinaryPath();
if (!existsSync(uvPath)) {
return null;
}
return new Promise((resolve) => {
const child = spawn(uvPath, ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PYTHON_VERSION,
});
let stdout = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
// Parse "uv 0.4.0" -> "0.4.0"
const match = stdout.match(/uv\s+(\S+)/);
resolve(match ? match[1] : stdout.trim());
} else {
resolve(null);
}
});
child.on('error', () => {
resolve(null);
});
});
}
/**
* Download and install UV using the official installation script
* @returns True if installation succeeded
*/
export async function ensureUvInstalled(): Promise<boolean> {
// Check if already installed
if (await isUvAvailable()) {
return true;
}
console.log('[UV] Installing UV package manager...');
return new Promise((resolve) => {
let child: ReturnType<typeof spawn>;
if (IS_WINDOWS) {
// Windows: Use PowerShell to run the install script
const installCmd = 'irm https://astral.sh/uv/install.ps1 | iex';
child = spawn('powershell', ['-ExecutionPolicy', 'ByPass', '-Command', installCmd], {
stdio: 'inherit',
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
});
} else {
// Unix: Use curl and sh
const installCmd = 'curl -LsSf https://astral.sh/uv/install.sh | sh';
child = spawn('sh', ['-c', installCmd], {
stdio: 'inherit',
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
});
}
child.on('close', (code) => {
if (code === 0) {
console.log('[UV] UV installed successfully');
resolve(true);
} else {
console.error(`[UV] Installation failed with code ${code}`);
resolve(false);
}
});
child.on('error', (err) => {
console.error(`[UV] Installation failed: ${err.message}`);
resolve(false);
});
});
}
/**
* UvManager class for virtual environment and package management
*/
export class UvManager {
private readonly venvPath: string;
private readonly pythonVersion?: string;
/**
* Create a new UvManager instance
* @param config - Configuration options
*/
constructor(config: UvManagerConfig) {
this.venvPath = config.venvPath;
this.pythonVersion = config.pythonVersion;
}
/**
* Get the path to the Python executable inside the virtual environment
* @returns Path to the Python executable
*/
getVenvPython(): string {
return join(this.venvPath, VENV_BIN_DIR, PYTHON_EXECUTABLE);
}
/**
* Get the path to pip inside the virtual environment
* @returns Path to the pip executable
*/
getVenvPip(): string {
const pipName = IS_WINDOWS ? 'pip.exe' : 'pip';
return join(this.venvPath, VENV_BIN_DIR, pipName);
}
/**
* Check if the virtual environment exists and is valid
* @returns True if the venv exists and has a working Python
*/
isVenvValid(): boolean {
const pythonPath = this.getVenvPython();
return existsSync(pythonPath);
}
/**
* Create a virtual environment using UV
* @returns Installation result
*/
async createVenv(): Promise<UvInstallResult> {
const startTime = Date.now();
// Ensure UV is available
if (!(await isUvAvailable())) {
const installed = await ensureUvInstalled();
if (!installed) {
return { success: false, error: 'Failed to install UV' };
}
}
const uvPath = getUvBinaryPath();
// Ensure parent directory exists
const parentDir = dirname(this.venvPath);
if (!existsSync(parentDir)) {
mkdirSync(parentDir, { recursive: true });
}
return new Promise((resolve) => {
const args = ['venv', this.venvPath];
// Add Python version constraint if specified
if (this.pythonVersion) {
args.push('--python', this.pythonVersion);
}
console.log(`[UV] Creating virtual environment at ${this.venvPath}`);
if (this.pythonVersion) {
console.log(`[UV] Python version: ${this.pythonVersion}`);
}
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
});
let stderr = '';
child.stdout.on('data', (data) => {
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
console.log(`[UV] Virtual environment created successfully (${duration}ms)`);
resolve({ success: true, duration });
} else {
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
}
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
});
});
}
/**
* Install packages from a local project with optional extras
* Uses `uv pip install -e` for editable installs
* @param projectPath - Path to the project directory (must contain pyproject.toml or setup.py)
* @param extras - Optional array of extras to install (e.g., ['semantic', 'dev'])
* @returns Installation result
*/
async installFromProject(projectPath: string, extras?: string[]): Promise<UvInstallResult> {
const startTime = Date.now();
// Ensure UV is available
if (!(await isUvAvailable())) {
return { success: false, error: 'UV is not available' };
}
// Ensure venv exists
if (!this.isVenvValid()) {
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
}
const uvPath = getUvBinaryPath();
// Build the install specifier
let installSpec = projectPath;
if (extras && extras.length > 0) {
installSpec = `${projectPath}[${extras.join(',')}]`;
}
return new Promise((resolve) => {
const args = ['pip', 'install', '-e', installSpec, '--python', this.getVenvPython()];
console.log(`[UV] Installing from project: ${installSpec}`);
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
cwd: projectPath,
});
let stderr = '';
child.stdout.on('data', (data) => {
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
const line = data.toString().trim();
if (line && !line.startsWith('Resolved') && !line.startsWith('Prepared') && !line.startsWith('Installed')) {
// Only log non-progress lines to stderr
console.log(`[UV] ${line}`);
}
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
console.log(`[UV] Project installation successful (${duration}ms)`);
resolve({ success: true, duration });
} else {
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
}
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
});
});
}
/**
* Install a list of packages
* @param packages - Array of package specifiers (e.g., ['numpy>=1.24', 'requests'])
* @returns Installation result
*/
async install(packages: string[]): Promise<UvInstallResult> {
const startTime = Date.now();
if (packages.length === 0) {
return { success: true, duration: 0 };
}
// Ensure UV is available
if (!(await isUvAvailable())) {
return { success: false, error: 'UV is not available' };
}
// Ensure venv exists
if (!this.isVenvValid()) {
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
}
const uvPath = getUvBinaryPath();
return new Promise((resolve) => {
const args = ['pip', 'install', ...packages, '--python', this.getVenvPython()];
console.log(`[UV] Installing packages: ${packages.join(', ')}`);
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
});
let stderr = '';
child.stdout.on('data', (data) => {
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
console.log(`[UV] Package installation successful (${duration}ms)`);
resolve({ success: true, duration });
} else {
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
}
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
});
});
}
/**
* Uninstall packages
* @param packages - Array of package names to uninstall
* @returns Uninstall result
*/
async uninstall(packages: string[]): Promise<UvInstallResult> {
const startTime = Date.now();
if (packages.length === 0) {
return { success: true, duration: 0 };
}
// Ensure UV is available
if (!(await isUvAvailable())) {
return { success: false, error: 'UV is not available' };
}
// Ensure venv exists
if (!this.isVenvValid()) {
return { success: false, error: 'Virtual environment does not exist.' };
}
const uvPath = getUvBinaryPath();
return new Promise((resolve) => {
const args = ['pip', 'uninstall', ...packages, '--python', this.getVenvPython()];
console.log(`[UV] Uninstalling packages: ${packages.join(', ')}`);
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
});
let stderr = '';
child.stdout.on('data', (data) => {
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
console.log(`[UV] Package uninstallation successful (${duration}ms)`);
resolve({ success: true, duration });
} else {
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
}
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
});
});
}
/**
* Sync dependencies from a requirements file or pyproject.toml
* Uses `uv pip sync` for deterministic installs
* @param requirementsPath - Path to requirements.txt or pyproject.toml
* @returns Sync result
*/
async sync(requirementsPath: string): Promise<UvInstallResult> {
const startTime = Date.now();
// Ensure UV is available
if (!(await isUvAvailable())) {
return { success: false, error: 'UV is not available' };
}
// Ensure venv exists
if (!this.isVenvValid()) {
return { success: false, error: 'Virtual environment does not exist. Call createVenv() first.' };
}
const uvPath = getUvBinaryPath();
return new Promise((resolve) => {
const args = ['pip', 'sync', requirementsPath, '--python', this.getVenvPython()];
console.log(`[UV] Syncing dependencies from: ${requirementsPath}`);
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PACKAGE_INSTALL,
});
let stderr = '';
child.stdout.on('data', (data) => {
const line = data.toString().trim();
if (line) {
console.log(`[UV] ${line}`);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
console.log(`[UV] Sync successful (${duration}ms)`);
resolve({ success: true, duration });
} else {
resolve({ success: false, error: stderr || `Process exited with code ${code}`, duration });
}
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, error: `Failed to spawn UV: ${err.message}`, duration });
});
});
}
/**
* List installed packages in the virtual environment
* @returns List of installed packages or null on error
*/
async list(): Promise<{ name: string; version: string }[] | null> {
// Ensure UV is available
if (!(await isUvAvailable())) {
return null;
}
// Ensure venv exists
if (!this.isVenvValid()) {
return null;
}
const uvPath = getUvBinaryPath();
return new Promise((resolve) => {
const args = ['pip', 'list', '--format', 'json', '--python', this.getVenvPython()];
const child = spawn(uvPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
});
let stdout = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
try {
const packages = JSON.parse(stdout);
resolve(packages);
} catch {
resolve(null);
}
} else {
resolve(null);
}
});
child.on('error', () => {
resolve(null);
});
});
}
/**
* Check if a specific package is installed
* @param packageName - Name of the package to check
* @returns True if the package is installed
*/
async isPackageInstalled(packageName: string): Promise<boolean> {
const packages = await this.list();
if (!packages) {
return false;
}
const normalizedName = packageName.toLowerCase().replace(/-/g, '_');
return packages.some(
(pkg) => pkg.name.toLowerCase().replace(/-/g, '_') === normalizedName
);
}
/**
* Run a Python command in the virtual environment
* @param args - Arguments to pass to Python
* @param options - Spawn options
* @returns Result with stdout/stderr
*/
async runPython(
args: string[],
options: { timeout?: number; cwd?: string } = {}
): Promise<{ success: boolean; stdout: string; stderr: string }> {
const pythonPath = this.getVenvPython();
if (!existsSync(pythonPath)) {
return { success: false, stdout: '', stderr: 'Virtual environment does not exist' };
}
return new Promise((resolve) => {
const child = spawn(pythonPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: options.timeout ?? EXEC_TIMEOUTS.PROCESS_SPAWN,
cwd: options.cwd,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim() });
});
child.on('error', (err) => {
resolve({ success: false, stdout: '', stderr: err.message });
});
});
}
/**
* Get Python version in the virtual environment
* @returns Python version string or null
*/
async getPythonVersion(): Promise<string | null> {
const result = await this.runPython(['--version']);
if (result.success) {
const match = result.stdout.match(/Python\s+(\S+)/);
return match ? match[1] : null;
}
return null;
}
/**
* Delete the virtual environment
* @returns True if deletion succeeded
*/
async deleteVenv(): Promise<boolean> {
if (!existsSync(this.venvPath)) {
return true;
}
try {
const fs = await import('fs');
fs.rmSync(this.venvPath, { recursive: true, force: true });
console.log(`[UV] Virtual environment deleted: ${this.venvPath}`);
return true;
} catch (err) {
console.error(`[UV] Failed to delete venv: ${(err as Error).message}`);
return false;
}
}
}
/**
* Create a UvManager with default settings for CodexLens
* @param dataDir - Base data directory (defaults to ~/.codexlens)
* @returns Configured UvManager instance
*/
export function createCodexLensUvManager(dataDir?: string): UvManager {
const baseDir = dataDir ?? join(homedir(), '.codexlens');
return new UvManager({
venvPath: join(baseDir, 'venv'),
pythonVersion: '>=3.10,<3.13', // onnxruntime compatibility
});
}
/**
* Quick bootstrap function: ensure UV is installed and create a venv
* @param venvPath - Path to the virtual environment
* @param pythonVersion - Optional Python version constraint
* @returns Installation result
*/
export async function bootstrapUvVenv(
venvPath: string,
pythonVersion?: string
): Promise<UvInstallResult> {
// Ensure UV is installed first
const uvInstalled = await ensureUvInstalled();
if (!uvInstalled) {
return { success: false, error: 'Failed to install UV' };
}
// Create the venv
const manager = new UvManager({ venvPath, pythonVersion });
return manager.createVenv();
}