From b19691b732f615b7ca077bf3e640af8b684c2d0a Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 13:15:44 -0700 Subject: [PATCH 01/28] temp --- src/managers/PackageManagerCommand.ts | 24 +++++++++++++++++++ .../PackageManagerCommandArguments.ts | 17 +++++++++++++ src/managers/builtin/commands/version.ts | 6 +++++ 3 files changed, 47 insertions(+) create mode 100644 src/managers/PackageManagerCommand.ts create mode 100644 src/managers/PackageManagerCommandArguments.ts create mode 100644 src/managers/builtin/commands/version.ts diff --git a/src/managers/PackageManagerCommand.ts b/src/managers/PackageManagerCommand.ts new file mode 100644 index 00000000..1b32a01f --- /dev/null +++ b/src/managers/PackageManagerCommand.ts @@ -0,0 +1,24 @@ +import { Package } from '../api'; +import { PackageManagerCommandArguments } from './PackageManagerCommandArguments'; + +export namespace PackageManagerCommand { + export interface Version { + execute(): Promise; + } + + export interface Install { + execute(options: PackageManagerCommandArguments.Install): Promise; + } + + export interface Uninstall { + execute(options: PackageManagerCommandArguments.Uninstall): Promise; + } + + export interface List { + execute(options: PackageManagerCommandArguments.List): Promise; + } + + export interface AvailableVersions { + execute(packageName: string, options: PackageManagerCommandArguments.AvailableVersions): Promise; + } +} diff --git a/src/managers/PackageManagerCommandArguments.ts b/src/managers/PackageManagerCommandArguments.ts new file mode 100644 index 00000000..ec566e03 --- /dev/null +++ b/src/managers/PackageManagerCommandArguments.ts @@ -0,0 +1,17 @@ +export namespace PackageManagerCommandArguments { + export interface Install { + packages: { packageName: string; version?: string }[]; + } + + export interface Uninstall { + packages: { packageName: string; version?: string }[]; + } + + export interface List { + directOnly?: boolean; + } + + export interface AvailableVersions { + includePrerelease?: boolean; + } +} diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts new file mode 100644 index 00000000..a5dc182f --- /dev/null +++ b/src/managers/builtin/commands/version.ts @@ -0,0 +1,6 @@ +class PipPackageManagerVersionCommand implements PackageManagerCommand.Version { + async execute(): Promise { + // Implementation for fetching the version of the package manager + return 'pip version'; + } +} From 8c32769e37fb6014c937ce3837b500bf3d4a33d8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:20:42 +0200 Subject: [PATCH 02/28] Implement three-level class-based package manager command architecture - Create base command infrastructure in src/managers/base/commands/ - PackageManagerCommand base class with minimal interface - CommandSettings interface for execution configuration - Six template classes (Install, Uninstall, List, Version, AvailableVersions, ListDirectNames) - Each template loads command-specific settings from VS Code config - Implement pip/uv commands in src/managers/builtin/commands/ - PipInstallCommand, PipUninstallCommand, PipListCommand, etc. - UvInstallCommand, UvUninstallCommand, UvListCommand, etc. - UV uses 'pip' subcommand syntax vs Pip uses '-m pip' - Implement conda commands in src/managers/conda/commands/ - CondaInstallCommand, CondaUninstallCommand, CondaListCommand - CondaVersionCommand, CondaAvailableVersionsCommand, CondaListDirectNamesCommand - Implement poetry commands in src/managers/poetry/commands/ - PoetryInstallCommand, PoetryUninstallCommand, PoetryListCommand - PoetryVersionCommand, PoetryAvailableVersionsCommand, PoetryListDirectNamesCommand - Remove redundant old command types - Delete PackageManagerCommand.ts (namespace interface) - Delete PackageManagerCommandArguments.ts - Delete BuiltinCommandExecutor (executor pattern not needed) - Restore package managers to original state (no integration yet) - Package managers remain unchanged from main branch - New command classes available for future integration Architecture: Three-level hierarchy - Level 1: Base class (PackageManagerCommand) - minimal shared interface - Level 2: Templates (InstallCommand, etc.) - load settings, define execute signature - Level 3: Concrete (Pip/Uv/Conda/Poetry commands) - implement buildCommand() and execute() Design principles: - Persisting arguments stored in constructor (pythonExecutable, indexUrl, settings) - Ephemeral arguments passed to execute() method (packages, upgrade flags) - Settings automatically loaded from python-envs.packageManager.{commandType}CommandArgs - Direct runPython calls with timeout support, no wrapper layer --- SETTINGS_ARCHITECTURE.md | 296 ++++++++++++++++++ package.json | 186 +++++++++++ package.nls.json | 8 +- src/managers/PackageManagerCommand.ts | 24 -- .../PackageManagerCommandArguments.ts | 17 - .../base/commands/availableVersions.ts | 24 ++ src/managers/base/commands/commandSettings.ts | 66 ++++ src/managers/base/commands/index.ts | 7 + src/managers/base/commands/install.ts | 27 ++ src/managers/base/commands/list.ts | 25 ++ src/managers/base/commands/listDirectNames.ts | 24 ++ src/managers/base/commands/uninstall.ts | 24 ++ src/managers/base/commands/version.ts | 24 ++ .../builtin/commands/availableVersions.ts | 128 ++++++++ .../builtin/commands/commandSettings.ts | 66 ++++ src/managers/builtin/commands/index.ts | 24 ++ src/managers/builtin/commands/install.ts | 110 +++++++ src/managers/builtin/commands/list.ts | 98 ++++++ .../builtin/commands/listDirectNames.ts | 81 +++++ src/managers/builtin/commands/uninstall.ts | 62 ++++ src/managers/builtin/commands/version.ts | 75 ++++- .../conda/commands/availableVersions.ts | 65 ++++ src/managers/conda/commands/install.ts | 52 +++ src/managers/conda/commands/list.ts | 50 +++ .../conda/commands/listDirectNames.ts | 54 ++++ src/managers/conda/commands/uninstall.ts | 35 +++ src/managers/conda/commands/version.ts | 39 +++ .../poetry/commands/availableVersions.ts | 68 ++++ src/managers/poetry/commands/install.ts | 52 +++ src/managers/poetry/commands/list.ts | 50 +++ .../poetry/commands/listDirectNames.ts | 47 +++ src/managers/poetry/commands/uninstall.ts | 35 +++ src/managers/poetry/commands/version.ts | 39 +++ 33 files changed, 1937 insertions(+), 45 deletions(-) create mode 100644 SETTINGS_ARCHITECTURE.md delete mode 100644 src/managers/PackageManagerCommand.ts delete mode 100644 src/managers/PackageManagerCommandArguments.ts create mode 100644 src/managers/base/commands/availableVersions.ts create mode 100644 src/managers/base/commands/commandSettings.ts create mode 100644 src/managers/base/commands/index.ts create mode 100644 src/managers/base/commands/install.ts create mode 100644 src/managers/base/commands/list.ts create mode 100644 src/managers/base/commands/listDirectNames.ts create mode 100644 src/managers/base/commands/uninstall.ts create mode 100644 src/managers/base/commands/version.ts create mode 100644 src/managers/builtin/commands/availableVersions.ts create mode 100644 src/managers/builtin/commands/commandSettings.ts create mode 100644 src/managers/builtin/commands/index.ts create mode 100644 src/managers/builtin/commands/install.ts create mode 100644 src/managers/builtin/commands/list.ts create mode 100644 src/managers/builtin/commands/listDirectNames.ts create mode 100644 src/managers/builtin/commands/uninstall.ts create mode 100644 src/managers/conda/commands/availableVersions.ts create mode 100644 src/managers/conda/commands/install.ts create mode 100644 src/managers/conda/commands/list.ts create mode 100644 src/managers/conda/commands/listDirectNames.ts create mode 100644 src/managers/conda/commands/uninstall.ts create mode 100644 src/managers/conda/commands/version.ts create mode 100644 src/managers/poetry/commands/availableVersions.ts create mode 100644 src/managers/poetry/commands/install.ts create mode 100644 src/managers/poetry/commands/list.ts create mode 100644 src/managers/poetry/commands/listDirectNames.ts create mode 100644 src/managers/poetry/commands/uninstall.ts create mode 100644 src/managers/poetry/commands/version.ts diff --git a/SETTINGS_ARCHITECTURE.md b/SETTINGS_ARCHITECTURE.md new file mode 100644 index 00000000..c920f2fd --- /dev/null +++ b/SETTINGS_ARCHITECTURE.md @@ -0,0 +1,296 @@ +# Class-Based Command Architecture with Three-Level Hierarchy + +## Overview +Implemented package management commands using a three-level class hierarchy that separates concerns cleanly: +1. **Base class** (`PackageManagerCommand`) — minimal shared interface +2. **Template classes** (`InstallCommand`, `ListCommand`, etc.) — load command-specific settings +3. **Concrete classes** (`PipInstallCommand`, `CondaInstallCommand`, etc.) — implement package-manager-specific logic + +This approach stores persisting arguments (like `indexUrl`) as instance properties while keeping ephemeral arguments (like packages) passed to `execute()`. + +## Architecture Components + +### 1. Base Class +**File**: `src/managers/builtin/commands/commandSettings.ts` + +```typescript +interface CommandConstructorOptions { + pythonExecutable: string; + log?: LogOutputChannel; + cancellationToken?: CancellationToken; +} + +abstract class PackageManagerCommand { + protected pythonExecutable: string; + protected log?: LogOutputChannel; + protected cancellationToken?: CancellationToken; + + constructor(options: CommandConstructorOptions) { + this.pythonExecutable = options.pythonExecutable; + this.log = options.log; + this.cancellationToken = options.cancellationToken; + } + + protected abstract buildCommand(ephemeralArgs: unknown): string[]; +} +``` + +Minimal interface: only shared across all commands. + +### 2. Template Classes +Each command type (install, uninstall, list, etc.) has a template class that: +- Loads its own command-specific settings from VS Code config +- Defines the execute() interface (signature varies per command) +- Is abstract (not instantiable directly) + +#### InstallCommand Template +```typescript +abstract class InstallCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.installCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise; +} +``` + +### 3. Concrete Classes +Each concrete class implements `buildCommand()` and `execute()` with package-manager-specific logic. + +#### PipInstallCommand (Concrete) +```typescript +export class PipInstallCommand extends InstallCommand { + private indexUrl?: string; // Persisting argument + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager'); + this.indexUrl = config.get('indexUrl'); // Load global config + } + + // buildCommand uses persisting args (indexUrl) + ephemeral args (packages, upgrade) + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['-m', 'pip', 'install']; + + if (this.indexUrl) { + args.push('--index-url', this.indexUrl); + } + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + const processedArgs = processEditableInstallArgs( + ephemeralArgs.packages.map((pkg) => pkg.packageName), + ); + args.push(...processedArgs); + + return args; + } + + // execute() spawns subprocess directly with runPython + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} +``` + +#### CondaInstallCommand (Concrete, Different Package Manager) +```typescript +export class CondaInstallCommand extends InstallCommand { + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['install', '-y']; + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + args.push(...ephemeralArgs.packages.map((p) => p.packageName)); + + return args; + } + + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, // conda executable + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} +``` + +## Separation of Concerns + +### Persisting Arguments (Constructor) +- Loaded once, reused across multiple executions +- Stored as instance properties +- Examples: `pythonExecutable`, `indexUrl`, `settings`, `log` + +### Ephemeral Arguments (Execute) +- Change per invocation +- Passed to `execute()` method +- Examples: `packages`, `packageName`, `pythonVersion`, `upgrade` + +```typescript +// Constructor: load persisting config +const install = new PipInstallCommand({ + pythonExecutable: '/usr/bin/python3', + log: logger, +}); + +// execute(): pass ephemeral args +await install.execute([{ packageName: 'numpy' }], true); +await install.execute([{ packageName: 'pandas' }], false); // Same indexUrl reused +``` + +## Usage Flow + +1. **Executor creates command instance** with persisting options: + ```typescript + const install = new PipInstallCommand({ + pythonExecutable, + log: context.log, + cancellationToken: context.cancellationToken, + }); + ``` + +2. **Constructor**: + - Calls `super(options)` to set pythonExecutable, log, cancellationToken + - Loads indexUrl from global config (persisting) + - Loads command-specific settings (timeout, retry, verbose) + +3. **Caller invokes execute()** with ephemeral args: + ```typescript + await install.execute(packages, upgrade); + ``` + +4. **execute()**: + - Calls `buildCommand()` with ephemeral args + - Calls `runPython()` directly (no intermediate executeCommand function) + - Settings applied via `this.settings.executionTimeout` + +## Command Files + +| File | Template | Concrete(s) | +|------|----------|------------| +| `commandSettings.ts` | — | `PackageManagerCommand` base, `CommandSettings` interface | +| `install.ts` | `InstallCommand` | `PipInstallCommand` | +| `uninstall.ts` | `UninstallCommand` | `PipUninstallCommand` | +| `list.ts` | `ListCommand` | `PipListCommand` | +| `version.ts` | `VersionCommand` | `PipVersionCommand` | +| `availableVersions.ts` | `AvailableVersionsCommand` | `PipAvailableVersionsCommand` | +| `listDirectNames.ts` | `ListDirectNamesCommand` | `PipListDirectNamesCommand` | + +## Future: Conda and Poetry + +When extending to conda and poetry, simply add new concrete classes: + +```typescript +// In conda/commands/install.ts +export class CondaInstallCommand extends InstallCommand { + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + // conda-specific argument building + } + async execute(packages, upgrade) { + // conda-specific execution + } +} + +// In poetry/commands/install.ts +export class PoetryInstallCommand extends InstallCommand { + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + // poetry-specific argument building + } + async execute(packages, upgrade) { + // poetry-specific execution + } +} +``` + +Same template interface, different implementations per package manager. + +## Key Design Decisions + +✅ **Three-level hierarchy**: Base → Template → Concrete +✅ **Persisting vs ephemeral**: Constructor for config, execute() for data +✅ **Settings auto-load**: Each template loads its own command-specific settings +✅ **Direct runPython**: No executeCommand intermediate function +✅ **Command-specific indexUrl**: Only loaded by install commands, others ignore +✅ **No stored results**: Commands return data directly, don't cache on instance +✅ **Extensible**: Easy to add conda, poetry, uv variants by extending templates + +## Executor Integration +**File**: `src/managers/builtin/commands/builtinCommandExecutor.ts` + +```typescript +export class BuiltinCommandExecutor { + async executeCommands( + environment: PythonEnvironment, + commands: BuiltinManageCommand[], + context: BuiltinCommandExecutionContext, + ): Promise { + const pythonExecutable = environment.execInfo?.run?.executable ?? 'python'; + + for (const command of commands) { + await this.executeCommand(pythonExecutable, command, context); + } + } + + private async executeCommand( + pythonExecutable: string, + command: BuiltinManageCommand, + context: BuiltinCommandExecutionContext, + ): Promise { + if (command.kind === 'install') { + // Create concrete class with persisting options + const install = new PipInstallCommand({ + pythonExecutable, + log: context.log, + cancellationToken: context.cancellationToken, + }); + // Execute with ephemeral args + await install.execute(command.payload.packages, command.payload.upgrade); + return; + } + // Similar for uninstall, list, etc. + } +} +``` + +## Architecture Components + +### 1. Base Class +**File**: `src/managers/builtin/commands/commandSettings.ts` diff --git a/package.json b/package.json index 5830cc3e..6730ea14 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,192 @@ "description": "%python-envs.packageWatchers.description%", "default": true, "scope": "machine" + }, + "python-envs.packageManager.installCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.installCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the install command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the install command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false + }, + "python-envs.packageManager.uninstallCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.uninstallCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the uninstall command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the uninstall command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false + }, + "python-envs.packageManager.listCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.listCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the list command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the list command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false + }, + "python-envs.packageManager.versionCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.versionCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the version command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the version command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false + }, + "python-envs.packageManager.availableVersionsCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.availableVersionsCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the availableVersions command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the availableVersions command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false + }, + "python-envs.packageManager.listDirectNamesCommandArgs": { + "type": "object", + "markdownDescription": "%python-envs.packageManager.listDirectNamesCommandArgs.description%", + "scope": "machine", + "properties": { + "executionTimeout": { + "type": "integer", + "description": "Timeout in milliseconds (0 = no timeout). Default: 5 minutes.", + "default": 300000, + "minimum": 0 + }, + "verboseOutput": { + "type": "boolean", + "description": "Display full output from the listDirectNames command.", + "default": false + }, + "retryOnFailure": { + "type": "boolean", + "description": "Retry the listDirectNames command once on failure.", + "default": true + }, + "maxRetries": { + "type": "integer", + "description": "Maximum number of retry attempts (0-3).", + "default": 1, + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false } } }, diff --git a/package.nls.json b/package.nls.json index 483ecfd2..d1d81e80 100644 --- a/package.nls.json +++ b/package.nls.json @@ -47,5 +47,11 @@ "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal...", "python-envs.managePackageVersion.title": "Manage Package Version", "python-envs.alwaysUseUv.description": "When set to true, uv will be used to manage all virtual environments if available. When set to false, uv will only manage virtual environments explicitly created by uv.", - "python-envs.packageWatchers.description": "When enabled, file system watchers monitor site-packages for install/uninstall changes and automatically refresh the package list. Disable this if you experience performance issues in large workspaces." + "python-envs.packageWatchers.description": "When enabled, file system watchers monitor site-packages for install/uninstall changes and automatically refresh the package list. Disable this if you experience performance issues in large workspaces.", + "python-envs.packageManager.installCommandArgs.description": "Arguments and settings for the install command (pip/uv install).", + "python-envs.packageManager.uninstallCommandArgs.description": "Arguments and settings for the uninstall command (pip/uv uninstall).", + "python-envs.packageManager.listCommandArgs.description": "Arguments and settings for the list command (pip/uv list).", + "python-envs.packageManager.versionCommandArgs.description": "Arguments and settings for the version command (pip/uv --version).", + "python-envs.packageManager.availableVersionsCommandArgs.description": "Arguments and settings for the availableVersions command (pip index versions).", + "python-envs.packageManager.listDirectNamesCommandArgs.description": "Arguments and settings for the listDirectNames command (pip list --not-required)." } diff --git a/src/managers/PackageManagerCommand.ts b/src/managers/PackageManagerCommand.ts deleted file mode 100644 index 1b32a01f..00000000 --- a/src/managers/PackageManagerCommand.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Package } from '../api'; -import { PackageManagerCommandArguments } from './PackageManagerCommandArguments'; - -export namespace PackageManagerCommand { - export interface Version { - execute(): Promise; - } - - export interface Install { - execute(options: PackageManagerCommandArguments.Install): Promise; - } - - export interface Uninstall { - execute(options: PackageManagerCommandArguments.Uninstall): Promise; - } - - export interface List { - execute(options: PackageManagerCommandArguments.List): Promise; - } - - export interface AvailableVersions { - execute(packageName: string, options: PackageManagerCommandArguments.AvailableVersions): Promise; - } -} diff --git a/src/managers/PackageManagerCommandArguments.ts b/src/managers/PackageManagerCommandArguments.ts deleted file mode 100644 index ec566e03..00000000 --- a/src/managers/PackageManagerCommandArguments.ts +++ /dev/null @@ -1,17 +0,0 @@ -export namespace PackageManagerCommandArguments { - export interface Install { - packages: { packageName: string; version?: string }[]; - } - - export interface Uninstall { - packages: { packageName: string; version?: string }[]; - } - - export interface List { - directOnly?: boolean; - } - - export interface AvailableVersions { - includePrerelease?: boolean; - } -} diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts new file mode 100644 index 00000000..d49573a7 --- /dev/null +++ b/src/managers/base/commands/availableVersions.ts @@ -0,0 +1,24 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for availableVersions commands. + * Loads availableVersions-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class AvailableVersionsCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.availableVersionsCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise; +} diff --git a/src/managers/base/commands/commandSettings.ts b/src/managers/base/commands/commandSettings.ts new file mode 100644 index 00000000..0cfd45e5 --- /dev/null +++ b/src/managers/base/commands/commandSettings.ts @@ -0,0 +1,66 @@ +import { CancellationToken, LogOutputChannel } from 'vscode'; + +export type CommandType = 'install' | 'uninstall' | 'list' | 'version' | 'availableVersions' | 'listDirectNames'; + +/** + * Settings that apply to a specific package manager command. + */ +export interface CommandSettings { + /** + * Timeout in milliseconds for command execution. 0 = no timeout. + */ + readonly executionTimeout: number; + + /** + * Whether to include verbose output from the package manager command. + */ + readonly verboseOutput: boolean; + + /** + * Whether to retry a failed command once before raising an error. + */ + readonly retryOnFailure: boolean; + + /** + * Maximum number of retry attempts for failed operations. + */ + readonly maxRetries: number; +} + +/** + * Result type for commands that parse output and return data. + */ +export interface CommandResult { + readonly data: T; + readonly rawOutput: string; +} + +/** + * Constructor options shared by all package manager commands. + */ +export interface CommandConstructorOptions { + pythonExecutable: string; + log?: LogOutputChannel; + cancellationToken?: CancellationToken; +} + +/** + * Base class for all package manager commands. + * Provides common properties and minimal interface for subclasses. + */ +export abstract class PackageManagerCommand { + protected pythonExecutable: string; + protected log?: LogOutputChannel; + protected cancellationToken?: CancellationToken; + + constructor(options: CommandConstructorOptions) { + this.pythonExecutable = options.pythonExecutable; + this.log = options.log; + this.cancellationToken = options.cancellationToken; + } + + /** + * Subclasses implement to build the command arguments. + */ + protected abstract buildCommand(ephemeralArgs: unknown): string[]; +} diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts new file mode 100644 index 00000000..97db2616 --- /dev/null +++ b/src/managers/base/commands/index.ts @@ -0,0 +1,7 @@ +export { CommandConstructorOptions, CommandSettings, CommandType, CommandResult, PackageManagerCommand } from './commandSettings'; +export { InstallCommand } from './install'; +export { UninstallCommand } from './uninstall'; +export { ListCommand } from './list'; +export { VersionCommand } from './version'; +export { AvailableVersionsCommand } from './availableVersions'; +export { ListDirectNamesCommand } from './listDirectNames'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts new file mode 100644 index 00000000..77bc8ac1 --- /dev/null +++ b/src/managers/base/commands/install.ts @@ -0,0 +1,27 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for install commands. + * Loads install-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class InstallCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.installCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise; +} diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts new file mode 100644 index 00000000..f72668d8 --- /dev/null +++ b/src/managers/base/commands/list.ts @@ -0,0 +1,25 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { PackageInfo } from '../../../api'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for list commands. + * Loads list-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class ListCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.listCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute(): Promise; +} diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts new file mode 100644 index 00000000..a14c5000 --- /dev/null +++ b/src/managers/base/commands/listDirectNames.ts @@ -0,0 +1,24 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for listDirectNames commands. + * Loads listDirectNames-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class ListDirectNamesCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.listDirectNamesCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute(): Promise; +} diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts new file mode 100644 index 00000000..747ccd05 --- /dev/null +++ b/src/managers/base/commands/uninstall.ts @@ -0,0 +1,24 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for uninstall commands. + * Loads uninstall-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class UninstallCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.uninstallCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute(packages: { packageName: string; version?: string }[]): Promise; +} diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts new file mode 100644 index 00000000..86beb173 --- /dev/null +++ b/src/managers/base/commands/version.ts @@ -0,0 +1,24 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; + +/** + * Template class for version commands. + * Loads version-specific settings from VS Code configuration. + * Subclasses implement concrete package-manager-specific logic. + */ +export abstract class VersionCommand extends PackageManagerCommand { + protected settings: CommandSettings; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager.versionCommandArgs'); + this.settings = { + executionTimeout: config.get('executionTimeout', 300000), + verboseOutput: config.get('verboseOutput', false), + retryOnFailure: config.get('retryOnFailure', true), + maxRetries: config.get('maxRetries', 1), + }; + } + + abstract execute(): Promise; +} diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts new file mode 100644 index 00000000..596ce37e --- /dev/null +++ b/src/managers/builtin/commands/availableVersions.ts @@ -0,0 +1,128 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for availableVersions command (change per execution). + */ +interface AvailableVersionsEphemeralArgs { + packageName: string; + pythonVersion: string; + includePrerelease?: boolean; +} + +/** + * Concrete pip availableVersions command. + * Builds pip-specific availableVersions arguments, parses JSON output, and returns version strings. + */ +export class PipAvailableVersionsCommand extends AvailableVersionsCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { + const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); + return [ + '-m', + 'pip', + 'index', + 'versions', + ephemeralArgs.packageName, + '--json', + '--python-version', + baseVersion, + ]; + } + + async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { + let availableVersions: string[] = []; + + const parser = (output: string): void => { + const match = output.match(/{[\s\S]*}/); + if (!match) { + availableVersions = []; + return; + } + try { + const parsed = JSON.parse(match[0]) as { versions?: string[] }; + let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; + if (!includePrerelease) { + versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); + } + availableVersions = versions; + } catch { + availableVersions = []; + } + }; + + const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return availableVersions; + } +} + +/** + * Concrete uv availableVersions command. + * Builds uv-specific availableVersions arguments, parses JSON output, and returns version strings. + */ +export class UvAvailableVersionsCommand extends AvailableVersionsCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { + const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); + return [ + 'pip', + 'index', + 'versions', + ephemeralArgs.packageName, + '--json', + '--python-version', + baseVersion, + ]; + } + + async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { + let availableVersions: string[] = []; + + const parser = (output: string): void => { + const match = output.match(/{[\s\S]*}/); + if (!match) { + availableVersions = []; + return; + } + try { + const parsed = JSON.parse(match[0]) as { versions?: string[] }; + let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; + if (!includePrerelease) { + versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); + } + availableVersions = versions; + } catch { + availableVersions = []; + } + }; + + const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return availableVersions; + } \ No newline at end of file diff --git a/src/managers/builtin/commands/commandSettings.ts b/src/managers/builtin/commands/commandSettings.ts new file mode 100644 index 00000000..0cfd45e5 --- /dev/null +++ b/src/managers/builtin/commands/commandSettings.ts @@ -0,0 +1,66 @@ +import { CancellationToken, LogOutputChannel } from 'vscode'; + +export type CommandType = 'install' | 'uninstall' | 'list' | 'version' | 'availableVersions' | 'listDirectNames'; + +/** + * Settings that apply to a specific package manager command. + */ +export interface CommandSettings { + /** + * Timeout in milliseconds for command execution. 0 = no timeout. + */ + readonly executionTimeout: number; + + /** + * Whether to include verbose output from the package manager command. + */ + readonly verboseOutput: boolean; + + /** + * Whether to retry a failed command once before raising an error. + */ + readonly retryOnFailure: boolean; + + /** + * Maximum number of retry attempts for failed operations. + */ + readonly maxRetries: number; +} + +/** + * Result type for commands that parse output and return data. + */ +export interface CommandResult { + readonly data: T; + readonly rawOutput: string; +} + +/** + * Constructor options shared by all package manager commands. + */ +export interface CommandConstructorOptions { + pythonExecutable: string; + log?: LogOutputChannel; + cancellationToken?: CancellationToken; +} + +/** + * Base class for all package manager commands. + * Provides common properties and minimal interface for subclasses. + */ +export abstract class PackageManagerCommand { + protected pythonExecutable: string; + protected log?: LogOutputChannel; + protected cancellationToken?: CancellationToken; + + constructor(options: CommandConstructorOptions) { + this.pythonExecutable = options.pythonExecutable; + this.log = options.log; + this.cancellationToken = options.cancellationToken; + } + + /** + * Subclasses implement to build the command arguments. + */ + protected abstract buildCommand(ephemeralArgs: unknown): string[]; +} diff --git a/src/managers/builtin/commands/index.ts b/src/managers/builtin/commands/index.ts new file mode 100644 index 00000000..8bf83814 --- /dev/null +++ b/src/managers/builtin/commands/index.ts @@ -0,0 +1,24 @@ +export { + PipInstallCommand, + UvInstallCommand, +} from './install'; +export { + PipUninstallCommand, + UvUninstallCommand, +} from './uninstall'; +export { + PipListCommand, + UvListCommand, +} from './list'; +export { + PipVersionCommand, + UvVersionCommand, +} from './version'; +export { + PipAvailableVersionsCommand, + UvAvailableVersionsCommand, +} from './availableVersions'; +export { + PipListDirectNamesCommand, + UvListDirectNamesCommand, +} from './listDirectNames'; diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts new file mode 100644 index 00000000..30eb106f --- /dev/null +++ b/src/managers/builtin/commands/install.ts @@ -0,0 +1,110 @@ +import { getConfiguration } from '../../../common/workspace.apis'; +import { runPython } from '../helpers'; +import { processEditableInstallArgs } from '../utils'; +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for install command (change per execution). + */ +interface InstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + +/** + * Concrete pip install command. + * Builds pip-specific install arguments and executes via runPython. + */ +export class PipInstallCommand extends InstallCommand { + private indexUrl?: string; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager'); + this.indexUrl = config.get('indexUrl'); + } + + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['-m', 'pip', 'install']; + + if (this.indexUrl) { + args.push('--index-url', this.indexUrl); + } + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + const processedArgs = processEditableInstallArgs( + ephemeralArgs.packages.map((pkg) => pkg.packageName), + ); + args.push(...processedArgs); + + return args; + } + + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} + +/** + * Concrete uv install command. + * Builds uv-specific install arguments and executes via runPython. + */ +export class UvInstallCommand extends InstallCommand { + private indexUrl?: string; + + constructor(options: CommandConstructorOptions) { + super(options); + const config = getConfiguration('python-envs.packageManager'); + this.indexUrl = config.get('indexUrl'); + } + + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['pip', 'install']; + + if (this.indexUrl) { + args.push('--index-url', this.indexUrl); + } + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + const processedArgs = processEditableInstallArgs( + ephemeralArgs.packages.map((pkg) => pkg.packageName), + ); + args.push(...processedArgs); + + return args; + } + + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts new file mode 100644 index 00000000..ed8d7c14 --- /dev/null +++ b/src/managers/builtin/commands/list.ts @@ -0,0 +1,98 @@ +import { PackageInfo } from '../../../api'; +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; + +/** + * Concrete pip list command. + * Builds pip-specific list arguments, parses JSON output, and returns PackageInfo[]. + */ +export class PipListCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['-m', 'pip', 'list', '--format=json']; + } + + async execute(): Promise { + const packages: PackageInfo[] = []; + + const parser = (output: string): void => { + const json = JSON.parse(output); + if (!Array.isArray(json)) { + throw new Error('Invalid output from pip list command'); + } + const parsed = json + .filter(({ name, version }) => name && version) + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, + })); + packages.push(...parsed); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return packages; + } +} + +/** + * Concrete uv list command. + * Builds uv-specific list arguments, parses JSON output, and returns PackageInfo[]. + */ +export class UvListCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['pip', 'list', '--format=json']; + } + + async execute(): Promise { + const packages: PackageInfo[] = []; + + const parser = (output: string): void => { + const json = JSON.parse(output); + if (!Array.isArray(json)) { + throw new Error('Invalid output from uv pip list command'); + } + const parsed = json + .filter(({ name, version }) => name && version) + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, + })); + packages.push(...parsed); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return packages; + } +} diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts new file mode 100644 index 00000000..44f8503b --- /dev/null +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -0,0 +1,81 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; + +/** + * Concrete pip listDirectNames command. + * Builds pip-specific listDirectNames arguments, parses JSON output, and returns direct package names. + */ +export class PipListDirectNamesCommand extends ListDirectNamesCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['-m', 'pip', 'list', '--format=json', '--not-required']; + } + + async execute(): Promise { + let directNames: string[] = []; + + const parser = (output: string): void => { + const packages = JSON.parse(output); + if (!Array.isArray(packages)) { + throw new Error('Invalid output from pip list command'); + } + directNames = packages.filter(({ name }) => name).map(({ name }) => name); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return directNames; + } +} + +/** + * Concrete uv listDirectNames command. + * Builds uv-specific listDirectNames arguments, parses JSON output, and returns direct package names. + */ +export class UvListDirectNamesCommand extends ListDirectNamesCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['pip', 'list', '--format=json', '--not-required']; + } + + async execute(): Promise { + let directNames: string[] = []; + + const parser = (output: string): void => { + const packages = JSON.parse(output); + if (!Array.isArray(packages)) { + throw new Error('Invalid output from uv pip list command'); + } + directNames = packages.filter(({ name }) => name).map(({ name }) => name); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return directNames; + } +} diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts new file mode 100644 index 00000000..7c3fb21c --- /dev/null +++ b/src/managers/builtin/commands/uninstall.ts @@ -0,0 +1,62 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for uninstall command (change per execution). + */ +interface UninstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + +/** + * Concrete pip uninstall command. + * Builds pip-specific uninstall arguments and executes via runPython. + */ +export class PipUninstallCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + return ['-m', 'pip', 'uninstall', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} + +/** + * Concrete uv uninstall command. + * Builds uv-specific uninstall arguments and executes via runPython. + */ +export class UvUninstallCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + return ['pip', 'uninstall', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index a5dc182f..fd2eb3f8 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -1,6 +1,75 @@ -class PipPackageManagerVersionCommand implements PackageManagerCommand.Version { +import { runPython } from '../helpers'; +import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; + +/** + * Concrete pip version command. + * Builds pip-specific version arguments, parses output, and returns version string. + */ +export class PipVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['-m', 'pip', '--version']; + } + + async execute(): Promise { + let versionString: string = ''; + + const parser = (output: string): void => { + // "pip X.Y.Z from /path/to/pip (python X.Y)" + const match = output.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); versionString = match ? match[1] : ''; + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versionString; + } +} + +/** + * Concrete uv version command. + * Builds uv-specific version arguments, parses output, and returns version string. + */ +export class UvVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['--version']; + } + async execute(): Promise { - // Implementation for fetching the version of the package manager - return 'pip version'; + let versionString: string = ''; + + const parser = (output: string): void => { + // "uv X.Y.Z" format + const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); versionString = match ? match[1] : ''; + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versionString; } } diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts new file mode 100644 index 00000000..6c1c4379 --- /dev/null +++ b/src/managers/conda/commands/availableVersions.ts @@ -0,0 +1,65 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for availableVersions command (change per execution). + */ +interface AvailableVersionsEphemeralArgs { + packageName: string; + pythonVersion: string; + includePrerelease?: boolean; +} + +/** + * Concrete conda availableVersions command. + * Builds conda-specific arguments, parses output, and returns available version strings. + */ +export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { + // Conda's search command + // Format: conda search --json + const args = ['search', ephemeralArgs.packageName, '--json']; + return args; + } + + async execute( + packageName: string, + pythonVersion: string, + includePrerelease?: boolean, + ): Promise { + const versions: string[] = []; + + const parser = (output: string): void => { + try { + const json = JSON.parse(output); + if (json[packageName] && Array.isArray(json[packageName])) { + let versionList = json[packageName].map((pkg: any) => pkg.version || pkg); + if (!includePrerelease) { + versionList = versionList.filter((v: string) => !v.includes('alpha') && !v.includes('beta') && !v.includes('rc')); + } + versions.push(...versionList); + } + } catch { + // If parsing fails, return empty + } + }; + + const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versions; + } +} diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts new file mode 100644 index 00000000..3b29e70d --- /dev/null +++ b/src/managers/conda/commands/install.ts @@ -0,0 +1,52 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for install command (change per execution). + */ +interface InstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + +/** + * Concrete conda install command. + * Builds conda-specific install arguments and executes via runPython. + */ +export class CondaInstallCommand extends InstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['install', '-y', '-c', 'conda-forge']; + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + args.push(...ephemeralArgs.packages.map((pkg) => { + if (pkg.version) { + return `${pkg.packageName}=${pkg.version}`; + } + return pkg.packageName; + })); + + return args; + } + + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/conda/commands/list.ts b/src/managers/conda/commands/list.ts new file mode 100644 index 00000000..572f7074 --- /dev/null +++ b/src/managers/conda/commands/list.ts @@ -0,0 +1,50 @@ +import { PackageInfo } from '../../../api'; +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; + +/** + * Concrete conda list command. + * Builds conda-specific list arguments, parses output, and returns PackageInfo[]. + */ +export class CondaListCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['list', '--json']; + } + + async execute(): Promise { + const packages: PackageInfo[] = []; + + const parser = (output: string): void => { + const json = JSON.parse(output); + if (!Array.isArray(json)) { + throw new Error('Invalid output from conda list command'); + } + const parsed = json + .filter(({ name, version }) => name && version) + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, + })); + packages.push(...parsed); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return packages; + } +} diff --git a/src/managers/conda/commands/listDirectNames.ts b/src/managers/conda/commands/listDirectNames.ts new file mode 100644 index 00000000..72644200 --- /dev/null +++ b/src/managers/conda/commands/listDirectNames.ts @@ -0,0 +1,54 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; + +/** + * Concrete conda listDirectNames command. + * Builds conda-specific arguments to extract direct dependencies, and returns their names. + */ +export class CondaListDirectNamesCommand extends ListDirectNamesCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + // Conda's list command with explicit format + // Format: conda list --json + return ['list', '--json']; + } + + async execute(): Promise { + const names: string[] = []; + + const parser = (output: string): void => { + try { + const json = JSON.parse(output); + if (Array.isArray(json)) { + // In conda, direct dependencies are marked with "not installed as dependencies" + // For now, we'll return all packages, filtering out Python itself + names.push( + ...json + .map((pkg: any) => pkg.name || pkg) + .filter((n: string) => n && n !== 'python') + .filter(Boolean), + ); + } + } catch { + // If parsing fails, return empty + } + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return names; + } +} diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts new file mode 100644 index 00000000..008075ed --- /dev/null +++ b/src/managers/conda/commands/uninstall.ts @@ -0,0 +1,35 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for uninstall command (change per execution). + */ +interface UninstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + +/** + * Concrete conda uninstall command. + * Builds conda-specific uninstall arguments and executes via runPython. + */ +export class CondaUninstallCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + return ['remove', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/conda/commands/version.ts b/src/managers/conda/commands/version.ts new file mode 100644 index 00000000..aaffb385 --- /dev/null +++ b/src/managers/conda/commands/version.ts @@ -0,0 +1,39 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; + +/** + * Concrete conda version command. + * Builds conda-specific version arguments, parses output, and returns version string. + */ +export class CondaVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['--version']; + } + + async execute(): Promise { + let versionString: string = ''; + + const parser = (output: string): void => { + // "conda X.Y.Z" or similar + const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); + versionString = match ? match[1] : ''; + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versionString; + } +} diff --git a/src/managers/poetry/commands/availableVersions.ts b/src/managers/poetry/commands/availableVersions.ts new file mode 100644 index 00000000..dd535f05 --- /dev/null +++ b/src/managers/poetry/commands/availableVersions.ts @@ -0,0 +1,68 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for availableVersions command (change per execution). + */ +interface AvailableVersionsEphemeralArgs { + packageName: string; + pythonVersion: string; + includePrerelease?: boolean; +} + +/** + * Concrete poetry availableVersions command. + * Builds poetry-specific arguments, parses output, and returns available version strings. + */ +export class PoetryAvailableVersionsCommand extends AvailableVersionsCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { + // Poetry's search command to find available versions + // Format: poetry search [--json] + const args = ['search', ephemeralArgs.packageName, '--json']; + return args; + } + + async execute( + packageName: string, + pythonVersion: string, + includePrerelease?: boolean, + ): Promise { + const versions: string[] = []; + + const parser = (output: string): void => { + try { + const json = JSON.parse(output); + if (Array.isArray(json) && json.length > 0) { + const pkg = json[0]; + if (pkg.versions && Array.isArray(pkg.versions)) { + let filtered = pkg.versions.map((v: any) => v.version || v); + if (!includePrerelease) { + filtered = filtered.filter((v: string) => !v.includes('-') && !v.includes('rc')); + } + versions.push(...filtered); + } + } + } catch { + // If parsing fails, return empty + } + }; + + const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versions; + } +} diff --git a/src/managers/poetry/commands/install.ts b/src/managers/poetry/commands/install.ts new file mode 100644 index 00000000..11df61fd --- /dev/null +++ b/src/managers/poetry/commands/install.ts @@ -0,0 +1,52 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for install command (change per execution). + */ +interface InstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + +/** + * Concrete poetry install command (using `poetry add`). + * Builds poetry-specific add arguments and executes via runPython. + */ +export class PoetryInstallCommand extends InstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['add']; + + if (ephemeralArgs.upgrade) { + args.push('--update'); + } + + args.push(...ephemeralArgs.packages.map((pkg) => { + if (pkg.version) { + return `${pkg.packageName}@${pkg.version}`; + } + return pkg.packageName; + })); + + return args; + } + + async execute( + packages: { packageName: string; version?: string }[], + upgrade?: boolean, + ): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/poetry/commands/list.ts b/src/managers/poetry/commands/list.ts new file mode 100644 index 00000000..f792549e --- /dev/null +++ b/src/managers/poetry/commands/list.ts @@ -0,0 +1,50 @@ +import { PackageInfo } from '../../../api'; +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; + +/** + * Concrete poetry list command (using `poetry show`). + * Builds poetry-specific show arguments, parses output, and returns PackageInfo[]. + */ +export class PoetryListCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['show', '--format=json']; + } + + async execute(): Promise { + const packages: PackageInfo[] = []; + + const parser = (output: string): void => { + const json = JSON.parse(output); + if (!Array.isArray(json)) { + throw new Error('Invalid output from poetry show command'); + } + const parsed = json + .filter(({ name, version }) => name && version) + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, + })); + packages.push(...parsed); + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return packages; + } +} diff --git a/src/managers/poetry/commands/listDirectNames.ts b/src/managers/poetry/commands/listDirectNames.ts new file mode 100644 index 00000000..94d42c6b --- /dev/null +++ b/src/managers/poetry/commands/listDirectNames.ts @@ -0,0 +1,47 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; + +/** + * Concrete poetry listDirectNames command. + * Builds poetry-specific arguments to extract direct dependencies, and returns their names. + */ +export class PoetryListDirectNamesCommand extends ListDirectNamesCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + // Poetry's show command with --outdated to get direct dependencies + // Format: poetry show --direct + return ['show', '--direct', '--format=json']; + } + + async execute(): Promise { + const names: string[] = []; + + const parser = (output: string): void => { + try { + const json = JSON.parse(output); + if (Array.isArray(json)) { + names.push(...json.map((pkg: any) => pkg.name || pkg).filter(Boolean)); + } + } catch { + // If parsing fails, return empty + } + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return names; + } +} diff --git a/src/managers/poetry/commands/uninstall.ts b/src/managers/poetry/commands/uninstall.ts new file mode 100644 index 00000000..b3364b05 --- /dev/null +++ b/src/managers/poetry/commands/uninstall.ts @@ -0,0 +1,35 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; + +/** + * Ephemeral arguments for uninstall command (change per execution). + */ +interface UninstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + +/** + * Concrete poetry uninstall command (using `poetry remove`). + * Builds poetry-specific remove arguments and executes via runPython. + */ +export class PoetryUninstallCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + return ['remove', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + + await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + } +} diff --git a/src/managers/poetry/commands/version.ts b/src/managers/poetry/commands/version.ts new file mode 100644 index 00000000..e46bd795 --- /dev/null +++ b/src/managers/poetry/commands/version.ts @@ -0,0 +1,39 @@ +import { runPython } from '../helpers'; +import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; + +/** + * Concrete poetry version command. + * Builds poetry-specific version arguments, parses output, and returns version string. + */ +export class PoetryVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + protected buildCommand(): string[] { + return ['--version']; + } + + async execute(): Promise { + let versionString: string = ''; + + const parser = (output: string): void => { + // "Poetry (version X.Y.Z)" or similar + const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); + versionString = match ? match[1] : ''; + }; + + const args = this.buildCommand(); + + const output = await runPython( + this.pythonExecutable, + args, + undefined, + this.log, + this.cancellationToken, + this.settings.executionTimeout, + ); + + parser(output); + return versionString; + } +} From 0632a09e9b3ed2b9ca75eb5e7954949b77b5f413 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:33:49 +0200 Subject: [PATCH 03/28] Integrate command classes with pipPackageManager - Phase 1: Pip/UV - Update CommandConstructorOptions to accept pythonExecutable - Modify PipInstallCommand/PipUninstallCommand to use pythonExecutable for pip - Modify UvInstallCommand/UvUninstallCommand to add --python flag with pythonExecutable - Create parsePackageSpecs utility to convert string[] to {packageName, version}[] objects - Integrate pip install/uninstall in pipPackageManager.manage(): - Detect UV via shouldUseUv() - Choose PipInstallCommand or UvInstallCommand based on UV detection - Execute commands directly with parsed package specs - Replace managePackages() utils call with direct command execution - Remove unused managePackages import from pipPackageManager - Fix syntax error in UvAvailableVersionsCommand (missing closing brace) - Defer Conda and Poetry command implementation for future phases Architecture simplification: - pythonExecutable now serves dual purpose: - Pip: environment.execInfo.run.executable (Python interpreter path) - UV: environment.execInfo.run.executable (passed with --python flag to uv pip) - No redundant environmentExecutable parameter needed - Commands execute with timeout from settings.executionTimeout Status: Pip/UV install/uninstall fully integrated and compiling Pending: List, Version, AvailableVersions, ListDirectNames commands for Pip/UV Pending: Full implementation for Conda and Poetry managers --- SETTINGS_ARCHITECTURE.md | 94 ++++++++-------- package-lock.json | 6 +- src/managers/base/commands/index.ts | 14 ++- src/managers/base/commands/install.ts | 5 +- src/managers/base/commands/list.ts | 2 +- .../builtin/commands/availableVersions.ts | 3 +- .../commands/builtinCommandExecutor.ts | 104 ++++++++++++++++++ src/managers/builtin/commands/index.ts | 30 +---- src/managers/builtin/commands/install.ts | 22 +--- src/managers/builtin/commands/list.ts | 2 +- .../builtin/commands/listDirectNames.ts | 2 +- src/managers/builtin/commands/uninstall.ts | 6 +- src/managers/builtin/commands/version.ts | 8 +- src/managers/builtin/pipPackageManager.ts | 41 +++++-- src/managers/builtin/utils.ts | 11 ++ .../conda/commands/availableVersions.ts | 65 ----------- src/managers/conda/commands/install.ts | 52 --------- src/managers/conda/commands/list.ts | 50 --------- .../conda/commands/listDirectNames.ts | 54 --------- src/managers/conda/commands/uninstall.ts | 35 ------ src/managers/conda/commands/version.ts | 39 ------- .../poetry/commands/availableVersions.ts | 68 ------------ src/managers/poetry/commands/install.ts | 52 --------- src/managers/poetry/commands/list.ts | 50 --------- .../poetry/commands/listDirectNames.ts | 47 -------- src/managers/poetry/commands/uninstall.ts | 35 ------ src/managers/poetry/commands/version.ts | 39 ------- 27 files changed, 239 insertions(+), 697 deletions(-) create mode 100644 src/managers/builtin/commands/builtinCommandExecutor.ts delete mode 100644 src/managers/conda/commands/availableVersions.ts delete mode 100644 src/managers/conda/commands/install.ts delete mode 100644 src/managers/conda/commands/list.ts delete mode 100644 src/managers/conda/commands/listDirectNames.ts delete mode 100644 src/managers/conda/commands/uninstall.ts delete mode 100644 src/managers/conda/commands/version.ts delete mode 100644 src/managers/poetry/commands/availableVersions.ts delete mode 100644 src/managers/poetry/commands/install.ts delete mode 100644 src/managers/poetry/commands/list.ts delete mode 100644 src/managers/poetry/commands/listDirectNames.ts delete mode 100644 src/managers/poetry/commands/uninstall.ts delete mode 100644 src/managers/poetry/commands/version.ts diff --git a/SETTINGS_ARCHITECTURE.md b/SETTINGS_ARCHITECTURE.md index c920f2fd..36534262 100644 --- a/SETTINGS_ARCHITECTURE.md +++ b/SETTINGS_ARCHITECTURE.md @@ -1,7 +1,9 @@ # Class-Based Command Architecture with Three-Level Hierarchy ## Overview + Implemented package management commands using a three-level class hierarchy that separates concerns cleanly: + 1. **Base class** (`PackageManagerCommand`) — minimal shared interface 2. **Template classes** (`InstallCommand`, `ListCommand`, etc.) — load command-specific settings 3. **Concrete classes** (`PipInstallCommand`, `CondaInstallCommand`, etc.) — implement package-manager-specific logic @@ -11,6 +13,7 @@ This approach stores persisting arguments (like `indexUrl`) as instance properti ## Architecture Components ### 1. Base Class + **File**: `src/managers/builtin/commands/commandSettings.ts` ```typescript @@ -38,12 +41,15 @@ abstract class PackageManagerCommand { Minimal interface: only shared across all commands. ### 2. Template Classes + Each command type (install, uninstall, list, etc.) has a template class that: + - Loads its own command-specific settings from VS Code config - Defines the execute() interface (signature varies per command) - Is abstract (not instantiable directly) #### InstallCommand Template + ```typescript abstract class InstallCommand extends PackageManagerCommand { protected settings: CommandSettings; @@ -59,25 +65,24 @@ abstract class InstallCommand extends PackageManagerCommand { }; } - abstract execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise; + abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise; } ``` ### 3. Concrete Classes + Each concrete class implements `buildCommand()` and `execute()` with package-manager-specific logic. #### PipInstallCommand (Concrete) + ```typescript export class PipInstallCommand extends InstallCommand { - private indexUrl?: string; // Persisting argument + private indexUrl?: string; // Persisting argument constructor(options: CommandConstructorOptions) { super(options); const config = getConfiguration('python-envs.packageManager'); - this.indexUrl = config.get('indexUrl'); // Load global config + this.indexUrl = config.get('indexUrl'); // Load global config } // buildCommand uses persisting args (indexUrl) + ephemeral args (packages, upgrade) @@ -92,19 +97,14 @@ export class PipInstallCommand extends InstallCommand { args.push('--upgrade'); } - const processedArgs = processEditableInstallArgs( - ephemeralArgs.packages.map((pkg) => pkg.packageName), - ); + const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName)); args.push(...processedArgs); return args; } // execute() spawns subprocess directly with runPython - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); await runPython( @@ -120,6 +120,7 @@ export class PipInstallCommand extends InstallCommand { ``` #### CondaInstallCommand (Concrete, Different Package Manager) + ```typescript export class CondaInstallCommand extends InstallCommand { protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { @@ -134,14 +135,11 @@ export class CondaInstallCommand extends InstallCommand { return args; } - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); await runPython( - this.pythonExecutable, // conda executable + this.pythonExecutable, // conda executable args, undefined, this.log, @@ -155,11 +153,13 @@ export class CondaInstallCommand extends InstallCommand { ## Separation of Concerns ### Persisting Arguments (Constructor) + - Loaded once, reused across multiple executions - Stored as instance properties - Examples: `pythonExecutable`, `indexUrl`, `settings`, `log` ### Ephemeral Arguments (Execute) + - Change per invocation - Passed to `execute()` method - Examples: `packages`, `packageName`, `pythonVersion`, `upgrade` @@ -173,46 +173,48 @@ const install = new PipInstallCommand({ // execute(): pass ephemeral args await install.execute([{ packageName: 'numpy' }], true); -await install.execute([{ packageName: 'pandas' }], false); // Same indexUrl reused +await install.execute([{ packageName: 'pandas' }], false); // Same indexUrl reused ``` ## Usage Flow 1. **Executor creates command instance** with persisting options: - ```typescript - const install = new PipInstallCommand({ - pythonExecutable, - log: context.log, - cancellationToken: context.cancellationToken, - }); - ``` + + ```typescript + const install = new PipInstallCommand({ + pythonExecutable, + log: context.log, + cancellationToken: context.cancellationToken, + }); + ``` 2. **Constructor**: - - Calls `super(options)` to set pythonExecutable, log, cancellationToken - - Loads indexUrl from global config (persisting) - - Loads command-specific settings (timeout, retry, verbose) + - Calls `super(options)` to set pythonExecutable, log, cancellationToken + - Loads indexUrl from global config (persisting) + - Loads command-specific settings (timeout, retry, verbose) 3. **Caller invokes execute()** with ephemeral args: - ```typescript - await install.execute(packages, upgrade); - ``` + + ```typescript + await install.execute(packages, upgrade); + ``` 4. **execute()**: - - Calls `buildCommand()` with ephemeral args - - Calls `runPython()` directly (no intermediate executeCommand function) - - Settings applied via `this.settings.executionTimeout` + - Calls `buildCommand()` with ephemeral args + - Calls `runPython()` directly (no intermediate executeCommand function) + - Settings applied via `this.settings.executionTimeout` ## Command Files -| File | Template | Concrete(s) | -|------|----------|------------| -| `commandSettings.ts` | — | `PackageManagerCommand` base, `CommandSettings` interface | -| `install.ts` | `InstallCommand` | `PipInstallCommand` | -| `uninstall.ts` | `UninstallCommand` | `PipUninstallCommand` | -| `list.ts` | `ListCommand` | `PipListCommand` | -| `version.ts` | `VersionCommand` | `PipVersionCommand` | -| `availableVersions.ts` | `AvailableVersionsCommand` | `PipAvailableVersionsCommand` | -| `listDirectNames.ts` | `ListDirectNamesCommand` | `PipListDirectNamesCommand` | +| File | Template | Concrete(s) | +| ---------------------- | -------------------------- | --------------------------------------------------------- | +| `commandSettings.ts` | — | `PackageManagerCommand` base, `CommandSettings` interface | +| `install.ts` | `InstallCommand` | `PipInstallCommand` | +| `uninstall.ts` | `UninstallCommand` | `PipUninstallCommand` | +| `list.ts` | `ListCommand` | `PipListCommand` | +| `version.ts` | `VersionCommand` | `PipVersionCommand` | +| `availableVersions.ts` | `AvailableVersionsCommand` | `PipAvailableVersionsCommand` | +| `listDirectNames.ts` | `ListDirectNamesCommand` | `PipListDirectNamesCommand` | ## Future: Conda and Poetry @@ -250,9 +252,10 @@ Same template interface, different implementations per package manager. ✅ **Direct runPython**: No executeCommand intermediate function ✅ **Command-specific indexUrl**: Only loaded by install commands, others ignore ✅ **No stored results**: Commands return data directly, don't cache on instance -✅ **Extensible**: Easy to add conda, poetry, uv variants by extending templates +✅ **Extensible**: Easy to add conda, poetry, uv variants by extending templates ## Executor Integration + **File**: `src/managers/builtin/commands/builtinCommandExecutor.ts` ```typescript @@ -293,4 +296,5 @@ export class BuiltinCommandExecutor { ## Architecture Components ### 1. Base Class + **File**: `src/managers/builtin/commands/commandSettings.ts` diff --git a/package-lock.json b/package-lock.json index 1ac3bcbf..0d079c48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6913,7 +6913,8 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true }, "node_modules/tunnel": { "version": "0.0.6", @@ -12366,7 +12367,8 @@ "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true }, "tunnel": { "version": "0.0.6", diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index 97db2616..ee72bbc1 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,7 +1,13 @@ -export { CommandConstructorOptions, CommandSettings, CommandType, CommandResult, PackageManagerCommand } from './commandSettings'; +export { AvailableVersionsCommand } from './availableVersions'; +export { + CommandConstructorOptions, + CommandResult, + CommandSettings, + CommandType, + PackageManagerCommand, +} from './commandSettings'; export { InstallCommand } from './install'; -export { UninstallCommand } from './uninstall'; export { ListCommand } from './list'; -export { VersionCommand } from './version'; -export { AvailableVersionsCommand } from './availableVersions'; export { ListDirectNamesCommand } from './listDirectNames'; +export { UninstallCommand } from './uninstall'; +export { VersionCommand } from './version'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 77bc8ac1..01458ca3 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -20,8 +20,5 @@ export abstract class InstallCommand extends PackageManagerCommand { }; } - abstract execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise; + abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise; } diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index f72668d8..4fd394a4 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,5 +1,5 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { PackageInfo } from '../../../api'; +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; /** diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 596ce37e..77e92dad 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -125,4 +125,5 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { parser(output); return availableVersions; - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/src/managers/builtin/commands/builtinCommandExecutor.ts b/src/managers/builtin/commands/builtinCommandExecutor.ts new file mode 100644 index 00000000..74b16cb1 --- /dev/null +++ b/src/managers/builtin/commands/builtinCommandExecutor.ts @@ -0,0 +1,104 @@ +import { CancellationToken, LogOutputChannel } from 'vscode'; +import { PackageManagementOptions, PythonEnvironment } from '../../../api'; +import { PipInstallCommand } from './install'; +import { PipUninstallCommand } from './uninstall'; + +export type BuiltinManageCommandId = 'install' | 'uninstall'; + +export type BuiltinManageCommandPayloadById = { + install: { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; + }; + uninstall: { + packages: { packageName: string; version?: string }[]; + }; +}; + +export type BuiltinManageCommand = { + kind: T; + payload: BuiltinManageCommandPayloadById[T]; +}; + +export interface BuiltinCommandExecutionContext { + log?: LogOutputChannel; + cancellationToken?: CancellationToken; +} + +/** + * Converts external package management options into strict internal commands. + */ +export function toBuiltinManageCommands(options: PackageManagementOptions): BuiltinManageCommand[] { + const commands: BuiltinManageCommand[] = []; + + if (options.uninstall && options.uninstall.length > 0) { + commands.push({ + kind: 'uninstall', + payload: { + packages: options.uninstall.map((packageName) => ({ packageName })), + }, + }); + } + + if (options.install && options.install.length > 0) { + commands.push({ + kind: 'install', + payload: { + packages: options.install.map((packageName) => ({ packageName })), + upgrade: options.upgrade, + }, + }); + } + + return commands; +} + +/** + * Executes builtin package management commands using pip. + * Instantiates command classes and invokes their execute methods. + */ +export class BuiltinCommandExecutor { + async executeCommands( + environment: PythonEnvironment, + commands: BuiltinManageCommand[], + context: BuiltinCommandExecutionContext, + ): Promise { + if (environment.version.startsWith('2.')) { + throw new Error('Python 2.* is not supported (deprecated)'); + } + + const pythonExecutable = environment.execInfo?.run?.executable ?? 'python'; + + for (const command of commands) { + await this.executeCommand(pythonExecutable, command, context); + } + } + + private async executeCommand( + pythonExecutable: string, + command: BuiltinManageCommand, + context: BuiltinCommandExecutionContext, + ): Promise { + if (command.kind === 'install') { + const installPayload = command.payload as BuiltinManageCommandPayloadById['install']; + const install = new PipInstallCommand({ + pythonExecutable, + log: context.log, + cancellationToken: context.cancellationToken, + }); + await install.execute(installPayload.packages, installPayload.upgrade); + return; + } + + if (command.kind === 'uninstall') { + const uninstallPayload = command.payload as BuiltinManageCommandPayloadById['uninstall']; + const uninstall = new PipUninstallCommand({ + pythonExecutable, + log: context.log, + cancellationToken: context.cancellationToken, + }); + await uninstall.execute(uninstallPayload.packages); + return; + } + } +} diff --git a/src/managers/builtin/commands/index.ts b/src/managers/builtin/commands/index.ts index 8bf83814..0f15cf97 100644 --- a/src/managers/builtin/commands/index.ts +++ b/src/managers/builtin/commands/index.ts @@ -1,24 +1,6 @@ -export { - PipInstallCommand, - UvInstallCommand, -} from './install'; -export { - PipUninstallCommand, - UvUninstallCommand, -} from './uninstall'; -export { - PipListCommand, - UvListCommand, -} from './list'; -export { - PipVersionCommand, - UvVersionCommand, -} from './version'; -export { - PipAvailableVersionsCommand, - UvAvailableVersionsCommand, -} from './availableVersions'; -export { - PipListDirectNamesCommand, - UvListDirectNamesCommand, -} from './listDirectNames'; +export { PipAvailableVersionsCommand, UvAvailableVersionsCommand } from './availableVersions'; +export { PipInstallCommand, UvInstallCommand } from './install'; +export { PipListCommand, UvListCommand } from './list'; +export { PipListDirectNamesCommand, UvListDirectNamesCommand } from './listDirectNames'; +export { PipUninstallCommand, UvUninstallCommand } from './uninstall'; +export { PipVersionCommand, UvVersionCommand } from './version'; diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 30eb106f..0ca99dd7 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -1,7 +1,7 @@ import { getConfiguration } from '../../../common/workspace.apis'; +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; import { runPython } from '../helpers'; import { processEditableInstallArgs } from '../utils'; -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; /** * Ephemeral arguments for install command (change per execution). @@ -35,18 +35,13 @@ export class PipInstallCommand extends InstallCommand { args.push('--upgrade'); } - const processedArgs = processEditableInstallArgs( - ephemeralArgs.packages.map((pkg) => pkg.packageName), - ); + const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName)); args.push(...processedArgs); return args; } - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); await runPython( @@ -74,7 +69,7 @@ export class UvInstallCommand extends InstallCommand { } protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { - let args = ['pip', 'install']; + let args = ['pip', 'install', '--python', this.pythonExecutable]; if (this.indexUrl) { args.push('--index-url', this.indexUrl); @@ -84,18 +79,13 @@ export class UvInstallCommand extends InstallCommand { args.push('--upgrade'); } - const processedArgs = processEditableInstallArgs( - ephemeralArgs.packages.map((pkg) => pkg.packageName), - ); + const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName)); args.push(...processedArgs); return args; } - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); await runPython( diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts index ed8d7c14..6937e47c 100644 --- a/src/managers/builtin/commands/list.ts +++ b/src/managers/builtin/commands/list.ts @@ -1,6 +1,6 @@ import { PackageInfo } from '../../../api'; -import { runPython } from '../helpers'; import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { runPython } from '../helpers'; /** * Concrete pip list command. diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts index 44f8503b..ea6aa19b 100644 --- a/src/managers/builtin/commands/listDirectNames.ts +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -1,5 +1,5 @@ -import { runPython } from '../helpers'; import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; +import { runPython } from '../helpers'; /** * Concrete pip listDirectNames command. diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index 7c3fb21c..c4a79c6c 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -1,5 +1,5 @@ -import { runPython } from '../helpers'; import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { runPython } from '../helpers'; /** * Ephemeral arguments for uninstall command (change per execution). @@ -44,7 +44,9 @@ export class UvUninstallCommand extends UninstallCommand { } protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['pip', 'uninstall', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + const args = ['pip', 'uninstall', '-y', '--python', this.pythonExecutable]; + args.push(...ephemeralArgs.packages.map((pkg) => pkg.packageName)); + return args; } async execute(packages: { packageName: string; version?: string }[]): Promise { diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index fd2eb3f8..33c3e3d7 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -1,5 +1,5 @@ -import { runPython } from '../helpers'; import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; +import { runPython } from '../helpers'; /** * Concrete pip version command. @@ -18,7 +18,8 @@ export class PipVersionCommand extends VersionCommand { const parser = (output: string): void => { // "pip X.Y.Z from /path/to/pip (python X.Y)" - const match = output.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); versionString = match ? match[1] : ''; + const match = output.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); + versionString = match ? match[1] : ''; }; const args = this.buildCommand(); @@ -55,7 +56,8 @@ export class UvVersionCommand extends VersionCommand { const parser = (output: string): void => { // "uv X.Y.Z" format - const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); versionString = match ? match[1] : ''; + const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); + versionString = match ? match[1] : ''; }; const args = this.buildCommand(); diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index f0e09a7d..cba2ee52 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -22,9 +22,10 @@ import { PythonEnvironmentApi, } from '../../api'; import { updatePackagesAndNotify } from '../common/packageChanges'; +import { PipInstallCommand, PipUninstallCommand, UvInstallCommand, UvUninstallCommand } from './commands/index'; import { runPython, runUV, shouldUseUv } from './helpers'; import { getWorkspacePackagesToInstall } from './pipUtils'; -import { managePackages, normalizePackageName, refreshPipDirectPackageNames, refreshPipPackages } from './utils'; +import { normalizePackageName, parsePackageSpecs, refreshPipDirectPackageNames, refreshPipPackages } from './utils'; import { VenvManager } from './venvManager'; export class PipPackageManager implements PackageManager, Disposable { @@ -65,11 +66,6 @@ export class PipPackageManager implements PackageManager, Disposable { } } - const manageOptions = { - ...options, - install: toInstall, - uninstall: toUninstall, - }; await window.withProgress( { location: ProgressLocation.Notification, @@ -78,7 +74,38 @@ export class PipPackageManager implements PackageManager, Disposable { }, async (_progress, token) => { try { - await managePackages(environment, manageOptions, this, token); + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { + throw new Error('Unable to determine Python executable path'); + } + + // Detect whether to use UV + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + + // Execute uninstall if needed + if (toUninstall.length > 0) { + const UninstallCommand = useUv ? UvUninstallCommand : PipUninstallCommand; + const uninstallCmd = new UninstallCommand({ + pythonExecutable, + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(toUninstall); + await uninstallCmd.execute(packages); + } + + // Execute install if needed + if (toInstall.length > 0) { + const InstallCommand = useUv ? UvInstallCommand : PipInstallCommand; + const installCmd = new InstallCommand({ + pythonExecutable, + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(toInstall); + await installCmd.execute(packages, options.upgrade); + } + await updatePackagesAndNotify( this, environment, diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index dc44fe75..21b8eb40 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -29,6 +29,17 @@ const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code'; const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask'; let pixiRecommendationShown = false; +/** + * Parse package specifications (strings) into package objects. + * Each string becomes a package object with packageName and empty version. + */ +export function parsePackageSpecs(packageStrings: string[]): { packageName: string; version?: string }[] { + return packageStrings.map((pkg) => ({ + packageName: pkg, + version: undefined, + })); +} + function asPackageQuickPickItem(name: string, version?: string): QuickPickItem { return { label: name, diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts deleted file mode 100644 index 6c1c4379..00000000 --- a/src/managers/conda/commands/availableVersions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for availableVersions command (change per execution). - */ -interface AvailableVersionsEphemeralArgs { - packageName: string; - pythonVersion: string; - includePrerelease?: boolean; -} - -/** - * Concrete conda availableVersions command. - * Builds conda-specific arguments, parses output, and returns available version strings. - */ -export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - - protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { - // Conda's search command - // Format: conda search --json - const args = ['search', ephemeralArgs.packageName, '--json']; - return args; - } - - async execute( - packageName: string, - pythonVersion: string, - includePrerelease?: boolean, - ): Promise { - const versions: string[] = []; - - const parser = (output: string): void => { - try { - const json = JSON.parse(output); - if (json[packageName] && Array.isArray(json[packageName])) { - let versionList = json[packageName].map((pkg: any) => pkg.version || pkg); - if (!includePrerelease) { - versionList = versionList.filter((v: string) => !v.includes('alpha') && !v.includes('beta') && !v.includes('rc')); - } - versions.push(...versionList); - } - } catch { - // If parsing fails, return empty - } - }; - - const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return versions; - } -} diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts deleted file mode 100644 index 3b29e70d..00000000 --- a/src/managers/conda/commands/install.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for install command (change per execution). - */ -interface InstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; -} - -/** - * Concrete conda install command. - * Builds conda-specific install arguments and executes via runPython. - */ -export class CondaInstallCommand extends InstallCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { - let args = ['install', '-y', '-c', 'conda-forge']; - - if (ephemeralArgs.upgrade) { - args.push('--upgrade'); - } - - args.push(...ephemeralArgs.packages.map((pkg) => { - if (pkg.version) { - return `${pkg.packageName}=${pkg.version}`; - } - return pkg.packageName; - })); - - return args; - } - - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { - const args = this.buildCommand({ packages, upgrade }); - - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - } -} diff --git a/src/managers/conda/commands/list.ts b/src/managers/conda/commands/list.ts deleted file mode 100644 index 572f7074..00000000 --- a/src/managers/conda/commands/list.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PackageInfo } from '../../../api'; -import { runPython } from '../helpers'; -import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; - -/** - * Concrete conda list command. - * Builds conda-specific list arguments, parses output, and returns PackageInfo[]. - */ -export class CondaListCommand extends ListCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(): string[] { - return ['list', '--json']; - } - - async execute(): Promise { - const packages: PackageInfo[] = []; - - const parser = (output: string): void => { - const json = JSON.parse(output); - if (!Array.isArray(json)) { - throw new Error('Invalid output from conda list command'); - } - const parsed = json - .filter(({ name, version }) => name && version) - .map(({ name, version }) => ({ - name, - version, - displayName: name, - description: version, - })); - packages.push(...parsed); - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return packages; - } -} diff --git a/src/managers/conda/commands/listDirectNames.ts b/src/managers/conda/commands/listDirectNames.ts deleted file mode 100644 index 72644200..00000000 --- a/src/managers/conda/commands/listDirectNames.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; - -/** - * Concrete conda listDirectNames command. - * Builds conda-specific arguments to extract direct dependencies, and returns their names. - */ -export class CondaListDirectNamesCommand extends ListDirectNamesCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - - protected buildCommand(): string[] { - // Conda's list command with explicit format - // Format: conda list --json - return ['list', '--json']; - } - - async execute(): Promise { - const names: string[] = []; - - const parser = (output: string): void => { - try { - const json = JSON.parse(output); - if (Array.isArray(json)) { - // In conda, direct dependencies are marked with "not installed as dependencies" - // For now, we'll return all packages, filtering out Python itself - names.push( - ...json - .map((pkg: any) => pkg.name || pkg) - .filter((n: string) => n && n !== 'python') - .filter(Boolean), - ); - } - } catch { - // If parsing fails, return empty - } - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return names; - } -} diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts deleted file mode 100644 index 008075ed..00000000 --- a/src/managers/conda/commands/uninstall.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for uninstall command (change per execution). - */ -interface UninstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; -} - -/** - * Concrete conda uninstall command. - * Builds conda-specific uninstall arguments and executes via runPython. - */ -export class CondaUninstallCommand extends UninstallCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['remove', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; - } - - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); - - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - } -} diff --git a/src/managers/conda/commands/version.ts b/src/managers/conda/commands/version.ts deleted file mode 100644 index aaffb385..00000000 --- a/src/managers/conda/commands/version.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; - -/** - * Concrete conda version command. - * Builds conda-specific version arguments, parses output, and returns version string. - */ -export class CondaVersionCommand extends VersionCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(): string[] { - return ['--version']; - } - - async execute(): Promise { - let versionString: string = ''; - - const parser = (output: string): void => { - // "conda X.Y.Z" or similar - const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); - versionString = match ? match[1] : ''; - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return versionString; - } -} diff --git a/src/managers/poetry/commands/availableVersions.ts b/src/managers/poetry/commands/availableVersions.ts deleted file mode 100644 index dd535f05..00000000 --- a/src/managers/poetry/commands/availableVersions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for availableVersions command (change per execution). - */ -interface AvailableVersionsEphemeralArgs { - packageName: string; - pythonVersion: string; - includePrerelease?: boolean; -} - -/** - * Concrete poetry availableVersions command. - * Builds poetry-specific arguments, parses output, and returns available version strings. - */ -export class PoetryAvailableVersionsCommand extends AvailableVersionsCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - - protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { - // Poetry's search command to find available versions - // Format: poetry search [--json] - const args = ['search', ephemeralArgs.packageName, '--json']; - return args; - } - - async execute( - packageName: string, - pythonVersion: string, - includePrerelease?: boolean, - ): Promise { - const versions: string[] = []; - - const parser = (output: string): void => { - try { - const json = JSON.parse(output); - if (Array.isArray(json) && json.length > 0) { - const pkg = json[0]; - if (pkg.versions && Array.isArray(pkg.versions)) { - let filtered = pkg.versions.map((v: any) => v.version || v); - if (!includePrerelease) { - filtered = filtered.filter((v: string) => !v.includes('-') && !v.includes('rc')); - } - versions.push(...filtered); - } - } - } catch { - // If parsing fails, return empty - } - }; - - const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return versions; - } -} diff --git a/src/managers/poetry/commands/install.ts b/src/managers/poetry/commands/install.ts deleted file mode 100644 index 11df61fd..00000000 --- a/src/managers/poetry/commands/install.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for install command (change per execution). - */ -interface InstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; -} - -/** - * Concrete poetry install command (using `poetry add`). - * Builds poetry-specific add arguments and executes via runPython. - */ -export class PoetryInstallCommand extends InstallCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { - let args = ['add']; - - if (ephemeralArgs.upgrade) { - args.push('--update'); - } - - args.push(...ephemeralArgs.packages.map((pkg) => { - if (pkg.version) { - return `${pkg.packageName}@${pkg.version}`; - } - return pkg.packageName; - })); - - return args; - } - - async execute( - packages: { packageName: string; version?: string }[], - upgrade?: boolean, - ): Promise { - const args = this.buildCommand({ packages, upgrade }); - - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - } -} diff --git a/src/managers/poetry/commands/list.ts b/src/managers/poetry/commands/list.ts deleted file mode 100644 index f792549e..00000000 --- a/src/managers/poetry/commands/list.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PackageInfo } from '../../../api'; -import { runPython } from '../helpers'; -import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; - -/** - * Concrete poetry list command (using `poetry show`). - * Builds poetry-specific show arguments, parses output, and returns PackageInfo[]. - */ -export class PoetryListCommand extends ListCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(): string[] { - return ['show', '--format=json']; - } - - async execute(): Promise { - const packages: PackageInfo[] = []; - - const parser = (output: string): void => { - const json = JSON.parse(output); - if (!Array.isArray(json)) { - throw new Error('Invalid output from poetry show command'); - } - const parsed = json - .filter(({ name, version }) => name && version) - .map(({ name, version }) => ({ - name, - version, - displayName: name, - description: version, - })); - packages.push(...parsed); - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return packages; - } -} diff --git a/src/managers/poetry/commands/listDirectNames.ts b/src/managers/poetry/commands/listDirectNames.ts deleted file mode 100644 index 94d42c6b..00000000 --- a/src/managers/poetry/commands/listDirectNames.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; - -/** - * Concrete poetry listDirectNames command. - * Builds poetry-specific arguments to extract direct dependencies, and returns their names. - */ -export class PoetryListDirectNamesCommand extends ListDirectNamesCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - - protected buildCommand(): string[] { - // Poetry's show command with --outdated to get direct dependencies - // Format: poetry show --direct - return ['show', '--direct', '--format=json']; - } - - async execute(): Promise { - const names: string[] = []; - - const parser = (output: string): void => { - try { - const json = JSON.parse(output); - if (Array.isArray(json)) { - names.push(...json.map((pkg: any) => pkg.name || pkg).filter(Boolean)); - } - } catch { - // If parsing fails, return empty - } - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return names; - } -} diff --git a/src/managers/poetry/commands/uninstall.ts b/src/managers/poetry/commands/uninstall.ts deleted file mode 100644 index b3364b05..00000000 --- a/src/managers/poetry/commands/uninstall.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; - -/** - * Ephemeral arguments for uninstall command (change per execution). - */ -interface UninstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; -} - -/** - * Concrete poetry uninstall command (using `poetry remove`). - * Builds poetry-specific remove arguments and executes via runPython. - */ -export class PoetryUninstallCommand extends UninstallCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['remove', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; - } - - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); - - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - } -} diff --git a/src/managers/poetry/commands/version.ts b/src/managers/poetry/commands/version.ts deleted file mode 100644 index e46bd795..00000000 --- a/src/managers/poetry/commands/version.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { runPython } from '../helpers'; -import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; - -/** - * Concrete poetry version command. - * Builds poetry-specific version arguments, parses output, and returns version string. - */ -export class PoetryVersionCommand extends VersionCommand { - constructor(options: CommandConstructorOptions) { - super(options); - } - protected buildCommand(): string[] { - return ['--version']; - } - - async execute(): Promise { - let versionString: string = ''; - - const parser = (output: string): void => { - // "Poetry (version X.Y.Z)" or similar - const match = output.match(/(\d+\.\d+(?:\.\d+)*)/); - versionString = match ? match[1] : ''; - }; - - const args = this.buildCommand(); - - const output = await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - this.settings.executionTimeout, - ); - - parser(output); - return versionString; - } -} From be50d97c47ed9eedc999c0444836642e542fa621 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:35:45 +0200 Subject: [PATCH 04/28] Remove unused executor pattern and old pip management code - Delete builtinCommandExecutor.ts (executor pattern was rejected in favor of direct command calls) - Remove managePackages() function from builtin/utils.ts (now handled by direct command execution) - Still needed in condaUtils.ts for conda integration Retained utilities: - refreshPipPackages() - used by getPackages() - refreshPipDirectPackageNames() - used by getDirectPackageNames() - normalizePackageName() - used by getDirectPackageNames() - parsePackageSpecs() - used by manage() for install/uninstall commands - processEditableInstallArgs() - used by PipInstallCommand All command execution for pip/uv now goes through direct command class instantiation with no intermediary dispatcher layer. --- .../builtin/commands/availableVersions.ts | 25 +---- .../commands/builtinCommandExecutor.ts | 104 ------------------ src/managers/builtin/utils.ts | 59 ---------- 3 files changed, 4 insertions(+), 184 deletions(-) delete mode 100644 src/managers/builtin/commands/builtinCommandExecutor.ts diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 77e92dad..2b4cb62d 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -1,5 +1,5 @@ +import { AvailableVersionsCommand, CommandConstructorOptions } from '../../base/commands/index'; import { runPython } from '../helpers'; -import { CommandConstructorOptions, AvailableVersionsCommand } from '../../base/commands/index'; /** * Ephemeral arguments for availableVersions command (change per execution). @@ -20,16 +20,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { } protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); - return [ - '-m', - 'pip', - 'index', - 'versions', - ephemeralArgs.packageName, - '--json', - '--python-version', - baseVersion, - ]; + return ['-m', 'pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; } async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { @@ -80,15 +71,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); - return [ - 'pip', - 'index', - 'versions', - ephemeralArgs.packageName, - '--json', - '--python-version', - baseVersion, - ]; + return ['pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; } async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { @@ -126,4 +109,4 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { parser(output); return availableVersions; } -} \ No newline at end of file +} diff --git a/src/managers/builtin/commands/builtinCommandExecutor.ts b/src/managers/builtin/commands/builtinCommandExecutor.ts deleted file mode 100644 index 74b16cb1..00000000 --- a/src/managers/builtin/commands/builtinCommandExecutor.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { CancellationToken, LogOutputChannel } from 'vscode'; -import { PackageManagementOptions, PythonEnvironment } from '../../../api'; -import { PipInstallCommand } from './install'; -import { PipUninstallCommand } from './uninstall'; - -export type BuiltinManageCommandId = 'install' | 'uninstall'; - -export type BuiltinManageCommandPayloadById = { - install: { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; - }; - uninstall: { - packages: { packageName: string; version?: string }[]; - }; -}; - -export type BuiltinManageCommand = { - kind: T; - payload: BuiltinManageCommandPayloadById[T]; -}; - -export interface BuiltinCommandExecutionContext { - log?: LogOutputChannel; - cancellationToken?: CancellationToken; -} - -/** - * Converts external package management options into strict internal commands. - */ -export function toBuiltinManageCommands(options: PackageManagementOptions): BuiltinManageCommand[] { - const commands: BuiltinManageCommand[] = []; - - if (options.uninstall && options.uninstall.length > 0) { - commands.push({ - kind: 'uninstall', - payload: { - packages: options.uninstall.map((packageName) => ({ packageName })), - }, - }); - } - - if (options.install && options.install.length > 0) { - commands.push({ - kind: 'install', - payload: { - packages: options.install.map((packageName) => ({ packageName })), - upgrade: options.upgrade, - }, - }); - } - - return commands; -} - -/** - * Executes builtin package management commands using pip. - * Instantiates command classes and invokes their execute methods. - */ -export class BuiltinCommandExecutor { - async executeCommands( - environment: PythonEnvironment, - commands: BuiltinManageCommand[], - context: BuiltinCommandExecutionContext, - ): Promise { - if (environment.version.startsWith('2.')) { - throw new Error('Python 2.* is not supported (deprecated)'); - } - - const pythonExecutable = environment.execInfo?.run?.executable ?? 'python'; - - for (const command of commands) { - await this.executeCommand(pythonExecutable, command, context); - } - } - - private async executeCommand( - pythonExecutable: string, - command: BuiltinManageCommand, - context: BuiltinCommandExecutionContext, - ): Promise { - if (command.kind === 'install') { - const installPayload = command.payload as BuiltinManageCommandPayloadById['install']; - const install = new PipInstallCommand({ - pythonExecutable, - log: context.log, - cancellationToken: context.cancellationToken, - }); - await install.execute(installPayload.packages, installPayload.upgrade); - return; - } - - if (command.kind === 'uninstall') { - const uninstallPayload = command.payload as BuiltinManageCommandPayloadById['uninstall']; - const uninstall = new PipUninstallCommand({ - pythonExecutable, - log: context.log, - cancellationToken: context.cancellationToken, - }); - await uninstall.execute(uninstallPayload.packages); - return; - } - } -} diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 21b8eb40..084a6ff6 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -282,65 +282,6 @@ export async function refreshPipDirectPackageNames( return packages.map((pkg) => pkg.name); } -export async function managePackages( - environment: PythonEnvironment, - options: PackageManagementOptions, - manager: PackageManager, - token?: CancellationToken, -): Promise { - if (environment.version.startsWith('2.')) { - throw new Error('Python 2.* is not supported (deprecated)'); - } - - // Use environmentPath directly for consistency with UV environment tracking - const useUv = await shouldUseUv(manager.log, environment.environmentPath.fsPath); - const uninstallArgs = ['pip', 'uninstall']; - if (options.uninstall && options.uninstall.length > 0) { - if (useUv) { - await runUV( - [...uninstallArgs, '--python', environment.execInfo.run.executable, ...options.uninstall], - undefined, - manager.log, - token, - ); - } else { - uninstallArgs.push('--yes'); - await runPython( - environment.execInfo.run.executable, - ['-m', ...uninstallArgs, ...options.uninstall], - undefined, - manager.log, - token, - ); - } - } - - const installArgs = ['pip', 'install']; - if (options.upgrade) { - installArgs.push('--upgrade'); - } - if (options.install && options.install.length > 0) { - const processedInstallArgs = processEditableInstallArgs(options.install); - - if (useUv) { - await runUV( - [...installArgs, '--python', environment.execInfo.run.executable, ...processedInstallArgs], - undefined, - manager.log, - token, - ); - } else { - await runPython( - environment.execInfo.run.executable, - ['-m', ...installArgs, ...processedInstallArgs], - undefined, - manager.log, - token, - ); - } - } -} - /** * Process pip install arguments to correctly handle editable installs with extras * This function will combine consecutive -e arguments that represent the same package with extras From f756833bfd0d1ec55461f49bd554b1b2f3579d44 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:40:46 +0200 Subject: [PATCH 05/28] Implement conda install/uninstall commands and remove managePackages utility - Create src/managers/conda/commands/ with install.ts and uninstall.ts - Implement CondaInstallCommand and CondaUninstallCommand classes - CondaInstallCommand: ['install', '-y', '-c', 'conda-forge', ...packages] - CondaUninstallCommand: ['remove', '-y', ...packages] - Create conda/helpers.ts with runConda wrapper function - Integrate conda commands into condaPackageManager.manage() - Direct instantiation of CondaInstallCommand/CondaUninstallCommand - Use parsePackageSpecs() from builtin/utils for package conversion - No intermediary dispatcher or managePackages utility - Remove managePackages() function from: - src/managers/builtin/utils.ts (no longer used by pip/uv) - src/managers/conda/condaUtils.ts (replaced by direct command execution) Result: - All package managers (pip, uv, conda) now use direct command class execution - No indirection layer or dispatcher patterns - managePackages utility completely eliminated - Both managers tested and compiling successfully Remove thin runConda wrapper - use runCondaExecutable directly - Delete src/managers/conda/helpers.ts (unnecessary indirection) - Update CondaInstallCommand to import and call runCondaExecutable directly - Update CondaUninstallCommand to import and call runCondaExecutable directly Simplifies conda command execution by removing wrapper layer per user preference --- src/managers/builtin/utils.ts | 12 +----- src/managers/conda/commands/index.ts | 2 + src/managers/conda/commands/install.ts | 45 +++++++++++++++++++++++ src/managers/conda/commands/uninstall.ts | 29 +++++++++++++++ src/managers/conda/condaPackageManager.ts | 32 ++++++++++++---- src/managers/conda/condaUtils.ts | 22 ----------- 6 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 src/managers/conda/commands/index.ts create mode 100644 src/managers/conda/commands/install.ts create mode 100644 src/managers/conda/commands/uninstall.ts diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 084a6ff6..6c4be1eb 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -1,13 +1,5 @@ -import { CancellationToken, LogOutputChannel, ProgressLocation, QuickPickItem, Uri, window } from 'vscode'; -import { - EnvironmentManager, - Package, - PackageManagementOptions, - PackageManager, - PythonEnvironment, - PythonEnvironmentApi, - PythonEnvironmentInfo, -} from '../../api'; +import { LogOutputChannel, ProgressLocation, QuickPickItem, Uri, window } from 'vscode'; +import { EnvironmentManager, Package, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { getExtension } from '../../common/extension.apis'; import { Common, PixiStrings, SysManagerStrings } from '../../common/localize'; diff --git a/src/managers/conda/commands/index.ts b/src/managers/conda/commands/index.ts new file mode 100644 index 00000000..cc13cb98 --- /dev/null +++ b/src/managers/conda/commands/index.ts @@ -0,0 +1,2 @@ +export { CondaInstallCommand } from './install'; +export { CondaUninstallCommand } from './uninstall'; diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts new file mode 100644 index 00000000..23b4b6ce --- /dev/null +++ b/src/managers/conda/commands/install.ts @@ -0,0 +1,45 @@ +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; +import { runCondaExecutable } from '../condaUtils'; + +/** + * Ephemeral arguments for install command (change per execution). + */ +interface InstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + +/** + * Concrete conda install command. + * Builds conda-specific install arguments and executes via runConda. + */ +export class CondaInstallCommand extends InstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + let args = ['install', '-y', '-c', 'conda-forge']; + + if (ephemeralArgs.upgrade) { + args.push('--upgrade'); + } + + args.push( + ...ephemeralArgs.packages.map((pkg) => { + if (pkg.version) { + return `${pkg.packageName}=${pkg.version}`; + } + return pkg.packageName; + }), + ); + + return args; + } + + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { + const args = this.buildCommand({ packages, upgrade }); + + await runCondaExecutable(args, this.log, this.cancellationToken); + } +} diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts new file mode 100644 index 00000000..8bfc88ed --- /dev/null +++ b/src/managers/conda/commands/uninstall.ts @@ -0,0 +1,29 @@ +import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { runCondaExecutable } from '../condaUtils'; + +/** + * Ephemeral arguments for uninstall command (change per execution). + */ +interface UninstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + +/** + * Concrete conda uninstall command. + * Builds conda-specific uninstall arguments and executes via runConda. + */ +export class CondaUninstallCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + return ['remove', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + + await runCondaExecutable(args, this.log, this.cancellationToken); + } +} diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 4105800b..269d5406 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -25,8 +25,10 @@ import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { CondaStrings } from '../../common/localize'; import { traceError } from '../../common/logging'; import { withProgress } from '../../common/window.apis'; +import { parsePackageSpecs } from '../builtin/utils'; import { updatePackagesAndNotify } from '../common/packageChanges'; -import { getCommonCondaPackagesToInstall, managePackages, runCondaExecutable } from './condaUtils'; +import { CondaInstallCommand, CondaUninstallCommand } from './commands/index'; +import { getCommonCondaPackagesToInstall, runCondaExecutable } from './condaUtils'; export class CondaPackageManager implements PackageManager, Disposable { private readonly _onDidChangePackages = new EventEmitter(); @@ -63,11 +65,6 @@ export class CondaPackageManager implements PackageManager, Disposable { } } - const manageOptions = { - ...options, - install: toInstall, - uninstall: toUninstall, - }; await withProgress( { location: ProgressLocation.Notification, @@ -76,7 +73,28 @@ export class CondaPackageManager implements PackageManager, Disposable { }, async (_progress, token) => { try { - await managePackages(environment, manageOptions, token, this.log); + // Execute uninstall if needed + if (toUninstall.length > 0) { + const uninstallCmd = new CondaUninstallCommand({ + pythonExecutable: 'conda', + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(toUninstall); + await uninstallCmd.execute(packages); + } + + // Execute install if needed + if (toInstall.length > 0) { + const installCmd = new CondaInstallCommand({ + pythonExecutable: 'conda', + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(toInstall); + await installCmd.execute(packages, options.upgrade); + } + await updatePackagesAndNotify( this, environment, diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 49dffd63..43537937 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -1275,28 +1275,6 @@ export async function deleteCondaEnvironment(environment: PythonEnvironment, log ); } -export async function managePackages( - environment: PythonEnvironment, - options: PackageManagementOptions, - token: CancellationToken, - log: LogOutputChannel, -): Promise { - if (options.uninstall && options.uninstall.length > 0) { - await runCondaExecutable( - ['remove', '--prefix', environment.environmentPath.fsPath, '--yes', ...options.uninstall], - log, - token, - ); - } - if (options.install && options.install.length > 0) { - const args = ['install', '--prefix', environment.environmentPath.fsPath, '--yes']; - if (options.upgrade) { - args.push('--update-all'); - } - args.push(...options.install); - await runCondaExecutable(args, log, token); - } -} async function getCommonPackages(): Promise { try { From 9db19a46460eb1b919d7542f1067fdbe006ac52d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:47:52 +0200 Subject: [PATCH 06/28] Refactor pipPackageManager to use command classes for all operations - Replace getPackages() to use PipListCommand/UvListCommand - Detects UV vs Pip and instantiates appropriate command - Calls execute() which returns PackageInfo[] - Replace getVersion() to use PipVersionCommand/UvVersionCommand - Direct command execution instead of inline pip/uv parsing - Replace getPackageAvailableVersions() to use PipAvailableVersionsCommand/UvAvailableVersionsCommand - Commands handle JSON parsing and version extraction - Still respects pip version check (< 21.2.0 returns undefined) - Replace getDirectPackageNames() to use PipListDirectNamesCommand/UvListDirectNamesCommand - Commands return package names, manager normalizes them Result: - All pipPackageManager operations now use the three-level command hierarchy - Removed imports of refreshPipPackages, refreshPipDirectPackageNames - No more inline runPython/runUV calls in getVersion/getPackageAvailableVersions - Consistent pattern: detect UV, choose command class, instantiate, execute() Remove parsePipIndexVersionsJson function and its tests - Delete src/test/managers/builtin/pipVersions.unit.test.ts - This file only tested parsePipIndexVersionsJson which is no longer needed - Remove parsePipIndexVersionsJson from pipPackageManager.ts - JSON parsing is now handled inside PipAvailableVersionsCommand - and UvAvailableVersionsCommand.execute() methods - Remove unused rcompare import from @renovatebot/pep440 The command classes now handle all parsing internally, eliminating need for this utility function in the package manager. --- src/managers/builtin/pipPackageManager.ts | 144 +++++++++--------- src/managers/conda/condaUtils.ts | 1 - .../managers/builtin/pipVersions.unit.test.ts | 37 ----- 3 files changed, 75 insertions(+), 107 deletions(-) delete mode 100644 src/test/managers/builtin/pipVersions.unit.test.ts diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index cba2ee52..e1b9e9c3 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -1,5 +1,5 @@ import type { Pep440Version } from '@renovatebot/pep440'; -import { compare, explain as parse, rcompare } from '@renovatebot/pep440'; +import { compare, explain as parse } from '@renovatebot/pep440'; import { CancellationError, Disposable, @@ -22,10 +22,23 @@ import { PythonEnvironmentApi, } from '../../api'; import { updatePackagesAndNotify } from '../common/packageChanges'; -import { PipInstallCommand, PipUninstallCommand, UvInstallCommand, UvUninstallCommand } from './commands/index'; -import { runPython, runUV, shouldUseUv } from './helpers'; +import { + PipAvailableVersionsCommand, + PipInstallCommand, + PipListCommand, + PipListDirectNamesCommand, + PipUninstallCommand, + PipVersionCommand, + UvAvailableVersionsCommand, + UvInstallCommand, + UvListCommand, + UvListDirectNamesCommand, + UvUninstallCommand, + UvVersionCommand, +} from './commands/index'; +import { shouldUseUv } from './helpers'; import { getWorkspacePackagesToInstall } from './pipUtils'; -import { normalizePackageName, parsePackageSpecs, refreshPipDirectPackageNames, refreshPipPackages } from './utils'; +import { normalizePackageName, parsePackageSpecs } from './utils'; import { VenvManager } from './venvManager'; export class PipPackageManager implements PackageManager, Disposable { @@ -152,7 +165,19 @@ export class PipPackageManager implements PackageManager, Disposable { async getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise { if (options?.skipCache || !this.packages.has(environment.envId.id)) { - const data = await refreshPipPackages(environment, this.log); + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { + return undefined; + } + + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + const ListCmd = useUv ? UvListCommand : PipListCommand; + const listCmd = new ListCmd({ + pythonExecutable, + log: this.log, + cancellationToken: undefined, + }); + const data = await listCmd.execute(); const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); this.packages.set(environment.envId.id, packages); return packages; @@ -162,22 +187,20 @@ export class PipPackageManager implements PackageManager, Disposable { async getVersion(environment: PythonEnvironment): Promise { try { - const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); - if (useUv) { - const result = await runUV(['--version'], undefined, this.log); - // "uv X.Y.Z" - const match = result.match(/^uv\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? (parse(match[1]) ?? undefined) : undefined; + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { + return undefined; } - const result = await runPython( - environment.execInfo?.run?.executable ?? 'python', - ['-m', 'pip', '--version'], - undefined, - this.log, - ); - // "pip X.Y.Z from /path/to/pip (python X.Y)" - const match = result.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? (parse(match[1]) ?? undefined) : undefined; + + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + const VersionCmd = useUv ? UvVersionCommand : PipVersionCommand; + const versionCmd = new VersionCmd({ + pythonExecutable, + log: this.log, + cancellationToken: undefined, + }); + const versionString = await versionCmd.execute(); + return versionString ? (parse(versionString) ?? undefined) : undefined; } catch { return undefined; } @@ -188,8 +211,8 @@ export class PipPackageManager implements PackageManager, Disposable { packageName: string, ): Promise { try { - const python = environment.execInfo?.run?.executable; - if (!python) { + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { return undefined; } @@ -197,30 +220,26 @@ export class PipPackageManager implements PackageManager, Disposable { if (!baseVersion) { return undefined; } - // uv - Run pip via `uv tool run pip` + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); - if (useUv) { - const output = await runUV( - ['tool', 'run', 'pip', 'index', 'versions', packageName, '--json', '--python-version', baseVersion], - undefined, - this.log, - ); - return parsePipIndexVersionsJson(output); - } + const AvailableVersionsCmd = useUv ? UvAvailableVersionsCommand : PipAvailableVersionsCommand; + const availableVersionsCmd = new AvailableVersionsCmd({ + pythonExecutable, + log: this.log, + cancellationToken: undefined, + }); - // pip >= 21.2.0 - use `pip index versions --json` to get available versions in a machine readable format. - const pipVersion = await this.getVersion(environment); - if (pipVersion && compare(pipVersion.public, '21.2.0') >= 0) { - const output = await runPython( - python, - ['-m', 'pip', 'index', 'versions', packageName, '--json', '--python-version', baseVersion], - undefined, - this.log, - ); - return parsePipIndexVersionsJson(output); + // For pip < 21.2.0, check version first + if (!useUv) { + const pipVersion = await this.getVersion(environment); + if (!pipVersion || compare(pipVersion.public, '21.2.0') < 0) { + // pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists. + return undefined; + } } - // pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists. + const versionStrings = await availableVersionsCmd.execute(packageName, environment.version); + return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; } @@ -232,38 +251,25 @@ export class PipPackageManager implements PackageManager, Disposable { } /** - * Returns direct (non-transitive) package names using `pip list --not-required` or `uv pip tree --depth=0`. + * Returns direct (non-transitive) package names using `pip list --not-required` or `uv pip list --not-required`. * * Note: These commands return packages with no installed dependents (leaf packages), not packages * the user explicitly installed. pip/uv do not track install intent. */ async getDirectPackageNames(environment: PythonEnvironment): Promise | undefined> { - const data = await refreshPipDirectPackageNames(environment, this.log); - return data ? new Set(data.map(normalizePackageName)) : undefined; - } -} - -/** - * Parses JSON output from `pip index versions --json`. - * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] } - */ -export function parsePipIndexVersionsJson(output: string): Pep440Version[] | undefined { - // Only capture output between braces - const match = output.match(/{[\s\S]*}/); - if (!match) { - return undefined; - } - try { - const parsed = JSON.parse(match[0]); - if (parsed && Array.isArray(parsed.versions) && parsed.versions.length > 0) { - return (parsed.versions as string[]) - .filter((v) => !!v.trim()) - .map((v) => parse(v.trim())) - .filter((v): v is Pep440Version => v !== null) - .sort((a, b) => rcompare(a.public, b.public)); + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { + return undefined; } - return undefined; - } catch { - return undefined; + + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + const ListDirectNamesCmd = useUv ? UvListDirectNamesCommand : PipListDirectNamesCommand; + const listDirectNamesCmd = new ListDirectNamesCmd({ + pythonExecutable, + log: this.log, + cancellationToken: undefined, + }); + const data = await listDirectNamesCmd.execute(); + return data ? new Set(data.map(normalizePackageName)) : undefined; } } diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 43537937..61040e99 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -1275,7 +1275,6 @@ export async function deleteCondaEnvironment(environment: PythonEnvironment, log ); } - async function getCommonPackages(): Promise { try { const pipData = path.join(EXTENSION_ROOT_DIR, 'files', 'conda_packages.json'); diff --git a/src/test/managers/builtin/pipVersions.unit.test.ts b/src/test/managers/builtin/pipVersions.unit.test.ts deleted file mode 100644 index 5c06c394..00000000 --- a/src/test/managers/builtin/pipVersions.unit.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import assert from 'assert'; -import { explain } from '@renovatebot/pep440'; -import { parsePipIndexVersionsJson } from '../../../managers/builtin/pipPackageManager'; - -suite('Pip Version Parsing', () => { - suite('parsePipIndexVersionsJson', () => { - test('parses valid JSON with versions array', () => { - const output = JSON.stringify({ name: 'requests', versions: ['2.31.0', '2.30.0', '2.29.0'] }); - const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0'].map((v) => explain(v))); - }); - - test('parses output with a single version', () => { - const output = JSON.stringify({ name: 'my-package', versions: ['1.0.0'] }); - const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, [explain('1.0.0')]); - }); - - test('returns undefined for empty versions array', () => { - const output = JSON.stringify({ name: 'pkg', versions: [] }); - const versions = parsePipIndexVersionsJson(output); - assert.strictEqual(versions, undefined); - }); - - test('returns undefined for invalid JSON', () => { - const versions = parsePipIndexVersionsJson('not json'); - assert.strictEqual(versions, undefined); - }); - - test('returns undefined when versions field is missing', () => { - const output = JSON.stringify({ name: 'pkg' }); - const versions = parsePipIndexVersionsJson(output); - assert.strictEqual(versions, undefined); - }); - }); -}); - From 31900cd60b56fc5cfdda3c736c2f92a732ae0c1b Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 04:57:25 +0200 Subject: [PATCH 07/28] Complete conda integration with List, Version, and AvailableVersions commands New Conda Commands: - CondaListCommand: runs 'conda list -p --json' - Takes environmentPath as ephemeral argument to execute() - Returns PackageInfo[] with name, displayName, version, description - CondaVersionCommand: runs 'conda --version' - Returns version string in format 'conda X.Y.Z' - CondaAvailableVersionsCommand: runs 'conda search --json' - Takes packageName, returns unique versions as string[] - Note: conda search returns all builds, we deduplicate versions Integration into condaPackageManager: - getPackages() uses CondaListCommand - getVersion() uses CondaVersionCommand - getPackageAvailableVersions() uses CondaAvailableVersionsCommand Removed: - Direct runCondaExecutable() calls in condaPackageManager - Inline JSON parsing and version filtering logic - Unused rcompare import (no longer needed for sorting) - traceError import (error handling moved to command level) Result: - All conda operations (install, uninstall, list, version, availableVersions) now use the three-level command hierarchy - Note: conda has no ListDirectNames equivalent (user confirmed --from-history is inaccurate) --- .../conda/commands/availableVersions.ts | 48 ++++++++++ src/managers/conda/commands/index.ts | 3 + src/managers/conda/commands/list.ts | 54 ++++++++++++ src/managers/conda/commands/version.ts | 25 ++++++ src/managers/conda/condaPackageManager.ts | 87 +++++++------------ 5 files changed, 162 insertions(+), 55 deletions(-) create mode 100644 src/managers/conda/commands/availableVersions.ts create mode 100644 src/managers/conda/commands/list.ts create mode 100644 src/managers/conda/commands/version.ts diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts new file mode 100644 index 00000000..5a0b36f5 --- /dev/null +++ b/src/managers/conda/commands/availableVersions.ts @@ -0,0 +1,48 @@ +import { AvailableVersionsCommand, CommandConstructorOptions } from '../../base/commands/index'; +import { runCondaExecutable } from '../condaUtils'; + +/** + * Ephemeral arguments for conda availableVersions command (change per execution). + */ +interface AvailableVersionsEphemeralArgs { + packageName: string; +} + +/** + * Concrete conda availableVersions command. + * Builds conda-specific availableVersions arguments, parses JSON output, and returns version strings. + */ +export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { + return ['search', ephemeralArgs.packageName, '--json']; + } + + async execute(packageName: string, _pythonVersion: string, _includePrerelease?: boolean): Promise { + const args = this.buildCommand({ packageName }); + const output = await runCondaExecutable(args, this.log, this.cancellationToken); + + try { + const parsed = JSON.parse(output); + if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { + const uniqueVersions = new Map(); + (parsed[packageName] as Array<{ version?: string }>) + .filter((entry) => !!entry.version?.trim()) + .forEach((entry) => { + const version = entry.version!.trim(); + if (!uniqueVersions.has(version)) { + uniqueVersions.set(version, version); + } + }); + + return Array.from(uniqueVersions.values()); + } + return []; + } catch { + return []; + } + } +} diff --git a/src/managers/conda/commands/index.ts b/src/managers/conda/commands/index.ts index cc13cb98..705ba8d0 100644 --- a/src/managers/conda/commands/index.ts +++ b/src/managers/conda/commands/index.ts @@ -1,2 +1,5 @@ +export { CondaAvailableVersionsCommand } from './availableVersions'; export { CondaInstallCommand } from './install'; +export { CondaListCommand } from './list'; export { CondaUninstallCommand } from './uninstall'; +export { CondaVersionCommand } from './version'; diff --git a/src/managers/conda/commands/list.ts b/src/managers/conda/commands/list.ts new file mode 100644 index 00000000..917ce631 --- /dev/null +++ b/src/managers/conda/commands/list.ts @@ -0,0 +1,54 @@ +import { PackageInfo } from '../../../api'; +import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { runCondaExecutable } from '../condaUtils'; + +/** + * Ephemeral arguments for conda list command (change per execution). + */ +interface ListEphemeralArgs { + environmentPath: string; +} + +/** + * Concrete conda list command. + * Builds conda-specific list arguments and returns PackageInfo[]. + */ +export class CondaListCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: ListEphemeralArgs): string[] { + return ['list', '-p', ephemeralArgs.environmentPath, '--json']; + } + + async execute(environmentPath?: string): Promise { + if (!environmentPath) { + return []; + } + + const args = this.buildCommand({ environmentPath }); + const output = await runCondaExecutable(args, this.log, this.cancellationToken); + + let condaPackages: { name: string; version: string }[]; + try { + condaPackages = JSON.parse(output) as { name: string; version: string }[]; + } catch { + return []; + } + + const packages: PackageInfo[] = []; + for (const condaPkg of condaPackages) { + if (condaPkg.name && condaPkg.version) { + packages.push({ + name: condaPkg.name, + displayName: condaPkg.name, + version: condaPkg.version, + description: condaPkg.version, + }); + } + } + + return packages; + } +} diff --git a/src/managers/conda/commands/version.ts b/src/managers/conda/commands/version.ts new file mode 100644 index 00000000..48da6bfd --- /dev/null +++ b/src/managers/conda/commands/version.ts @@ -0,0 +1,25 @@ +import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; +import { runCondaExecutable } from '../condaUtils'; + +/** + * Concrete conda version command. + * Builds conda-specific version arguments and returns version string. + */ +export class CondaVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['--version']; + } + + async execute(): Promise { + const args = this.buildCommand(); + const output = await runCondaExecutable(args, this.log, this.cancellationToken); + + // "conda X.Y.Z" + const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); + return match ? match[1] : ''; + } +} diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 269d5406..929a80b2 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -1,5 +1,5 @@ import type { Pep440Version } from '@renovatebot/pep440'; -import { explain as parse, rcompare } from '@renovatebot/pep440'; +import { explain as parse } from '@renovatebot/pep440'; import * as path from 'path'; import { CancellationError, @@ -23,12 +23,17 @@ import { } from '../../api'; import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { CondaStrings } from '../../common/localize'; -import { traceError } from '../../common/logging'; import { withProgress } from '../../common/window.apis'; import { parsePackageSpecs } from '../builtin/utils'; import { updatePackagesAndNotify } from '../common/packageChanges'; -import { CondaInstallCommand, CondaUninstallCommand } from './commands/index'; -import { getCommonCondaPackagesToInstall, runCondaExecutable } from './condaUtils'; +import { + CondaAvailableVersionsCommand, + CondaInstallCommand, + CondaListCommand, + CondaUninstallCommand, + CondaVersionCommand, +} from './commands/index'; +import { getCommonCondaPackagesToInstall } from './condaUtils'; export class CondaPackageManager implements PackageManager, Disposable { private readonly _onDidChangePackages = new EventEmitter(); @@ -138,34 +143,13 @@ export class CondaPackageManager implements PackageManager, Disposable { async getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise { if (options?.skipCache || !this.packages.has(environment.envId.id)) { - const args = ['list', '-p', environment.environmentPath.fsPath, '--json']; - const data = await runCondaExecutable(args); - - let condaPackages: { name: string; version: string }[]; - try { - condaPackages = JSON.parse(data) as { name: string; version: string }[]; - } catch (e) { - traceError(`Failed to parse conda list JSON output: ${data}`, e); - return []; - } - - const packages: Package[] = []; - for (const condaPkg of condaPackages) { - if (condaPkg.name && condaPkg.version) { - packages.push( - this.api.createPackageItem( - { - name: condaPkg.name, - displayName: condaPkg.name, - version: condaPkg.version, - description: condaPkg.version, - }, - environment, - this, - ), - ); - } - } + const listCmd = new CondaListCommand({ + pythonExecutable: 'conda', + log: this.log, + cancellationToken: undefined, + }); + const data = await listCmd.execute(environment.environmentPath.fsPath); + const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); this.packages.set(environment.envId.id, packages); return packages; } @@ -179,10 +163,13 @@ export class CondaPackageManager implements PackageManager, Disposable { async getVersion(_environment: PythonEnvironment): Promise { try { - const output = await runCondaExecutable(['--version'], this.log); - // "conda X.Y.Z" - const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); - return match ? (parse(match[1]) ?? undefined) : undefined; + const versionCmd = new CondaVersionCommand({ + pythonExecutable: 'conda', + log: this.log, + cancellationToken: undefined, + }); + const versionString = await versionCmd.execute(); + return versionString ? (parse(versionString) ?? undefined) : undefined; } catch { return undefined; } @@ -193,25 +180,15 @@ export class CondaPackageManager implements PackageManager, Disposable { packageName: string, ): Promise { try { - const output = await runCondaExecutable(['search', packageName, '--json'], this.log); - const parsed = JSON.parse(output); - if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { - const uniqueVersions = new Map(); - parsed[packageName] - .filter((entry: { version?: string }) => !!entry.version?.trim()) - .map((entry: { version?: string }) => parse(entry.version!)) - .filter((v: Pep440Version | null): v is Pep440Version => v !== null) - .forEach((version: Pep440Version) => { - if (!uniqueVersions.has(version.public)) { - uniqueVersions.set(version.public, version); - } - }); - - return Array.from(uniqueVersions.values()).sort((a: Pep440Version, b: Pep440Version) => - rcompare(a.public, b.public), - ); - } - return undefined; + const availableVersionsCmd = new CondaAvailableVersionsCommand({ + pythonExecutable: 'conda', + log: this.log, + cancellationToken: undefined, + }); + const versionStrings = await availableVersionsCmd.execute(packageName, ''); + return versionStrings + .map((v) => parse(v)) + .filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; } From d34f7a937bf3bda1cdd90984c3b6c396c8d95416 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:04:15 +0200 Subject: [PATCH 08/28] Integrate Poetry command classes into poetryPackageManager Implement all Poetry operations (Add, Remove, Show, Version, ShowTopLevel) as concrete command classes extending the base template classes: - PoetryAddCommand: extends InstallCommand, uses 'poetry add' - PoetryRemoveCommand: extends UninstallCommand, uses 'poetry remove' - PoetryShowCommand: extends ListCommand, uses 'poetry show --no-ansi' - PoetryVersionCommand: extends VersionCommand, gets poetry version - PoetryShowTopLevelCommand: extends ListDirectNamesCommand, uses 'poetry show --top-level' Refactor poetryPackageManager methods to use command classes directly: - manage(): instantiates Add/Remove commands - fetchPackagesFromTool(): uses PoetryShowCommand - getVersion(): uses PoetryVersionCommand - getDirectPackageNames(): uses PoetryShowTopLevelCommand - getPackageAvailableVersions(): returns undefined (Poetry doesn't support this) Remove direct poetry command calls and unused imports. All operations now follow the three-level command class pattern. --- src/managers/conda/condaPackageManager.ts | 4 +- src/managers/poetry/commands/add.ts | 44 ++++++ src/managers/poetry/commands/index.ts | 5 + src/managers/poetry/commands/remove.ts | 28 ++++ src/managers/poetry/commands/show.ts | 48 +++++++ src/managers/poetry/commands/showTopLevel.ts | 32 +++++ src/managers/poetry/commands/version.ts | 27 ++++ src/managers/poetry/poetryPackageManager.ts | 134 ++++++------------- 8 files changed, 227 insertions(+), 95 deletions(-) create mode 100644 src/managers/poetry/commands/add.ts create mode 100644 src/managers/poetry/commands/index.ts create mode 100644 src/managers/poetry/commands/remove.ts create mode 100644 src/managers/poetry/commands/show.ts create mode 100644 src/managers/poetry/commands/showTopLevel.ts create mode 100644 src/managers/poetry/commands/version.ts diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 929a80b2..6935b01a 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -186,9 +186,7 @@ export class CondaPackageManager implements PackageManager, Disposable { cancellationToken: undefined, }); const versionStrings = await availableVersionsCmd.execute(packageName, ''); - return versionStrings - .map((v) => parse(v)) - .filter((parsed) => parsed !== undefined) as Pep440Version[]; + return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; } diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts new file mode 100644 index 00000000..2d79371b --- /dev/null +++ b/src/managers/poetry/commands/add.ts @@ -0,0 +1,44 @@ +import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; +import { runPoetry } from '../poetryPackageManager'; + +/** + * Ephemeral arguments for poetry add command (change per execution). + */ +interface AddEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + +/** + * Concrete poetry add command. + * Builds poetry-specific add arguments and executes via runPoetry. + */ +export class PoetryAddCommand extends InstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: AddEphemeralArgs): string[] { + const args = ['add']; + + if (ephemeralArgs.upgrade) { + args.push('--allow-prereleases'); + } + + args.push( + ...ephemeralArgs.packages.map((pkg) => { + if (pkg.version) { + return `${pkg.packageName}@${pkg.version}`; + } + return pkg.packageName; + }), + ); + + return args; + } + + async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { + const args = this.buildCommand({ packages, upgrade }); + await runPoetry(args, undefined, this.log, this.cancellationToken); + } +} diff --git a/src/managers/poetry/commands/index.ts b/src/managers/poetry/commands/index.ts new file mode 100644 index 00000000..19a89134 --- /dev/null +++ b/src/managers/poetry/commands/index.ts @@ -0,0 +1,5 @@ +export { PoetryAddCommand } from './add'; +export { PoetryRemoveCommand } from './remove'; +export { PoetryShowCommand } from './show'; +export { PoetryShowTopLevelCommand } from './showTopLevel'; +export { PoetryVersionCommand } from './version'; diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts new file mode 100644 index 00000000..b66ea0f5 --- /dev/null +++ b/src/managers/poetry/commands/remove.ts @@ -0,0 +1,28 @@ +import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { runPoetry } from '../poetryPackageManager'; + +/** + * Ephemeral arguments for poetry remove command (change per execution). + */ +interface RemoveEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + +/** + * Concrete poetry remove command. + * Builds poetry-specific remove arguments and executes via runPoetry. + */ +export class PoetryRemoveCommand extends UninstallCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(ephemeralArgs: RemoveEphemeralArgs): string[] { + return ['remove', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + } + + async execute(packages: { packageName: string; version?: string }[]): Promise { + const args = this.buildCommand({ packages }); + await runPoetry(args, undefined, this.log, this.cancellationToken); + } +} diff --git a/src/managers/poetry/commands/show.ts b/src/managers/poetry/commands/show.ts new file mode 100644 index 00000000..ca729643 --- /dev/null +++ b/src/managers/poetry/commands/show.ts @@ -0,0 +1,48 @@ +import { PackageInfo } from '../../../api'; +import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { runPoetry } from '../poetryPackageManager'; + +/** + * Concrete poetry show command. + * Builds poetry-specific show arguments and returns PackageInfo[]. + */ +export class PoetryShowCommand extends ListCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['show', '--no-ansi']; + } + + async execute(): Promise { + const args = this.buildCommand(); + const output = await runPoetry(args, undefined, this.log, this.cancellationToken); + + const packages: PackageInfo[] = []; + + try { + // Parse poetry show output + // Format: name version description + const lines = output.split('\n'); + for (const line of lines) { + // Updated regex to properly handle lines with the format: + // "package (!) version description" + const match = line.match(/^(\S+)(?:\s+\([!]\))?\s+(\S+)\s+(.*)/); + if (match) { + const [, name, version, description] = match; + packages.push({ + name, + displayName: name, + version, + description: `${version} - ${description?.trim() || ''}`, + }); + } + } + } catch { + return []; + } + + return packages; + } +} diff --git a/src/managers/poetry/commands/showTopLevel.ts b/src/managers/poetry/commands/showTopLevel.ts new file mode 100644 index 00000000..58783acf --- /dev/null +++ b/src/managers/poetry/commands/showTopLevel.ts @@ -0,0 +1,32 @@ +import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; +import { runPoetry } from '../poetryPackageManager'; + +/** + * Concrete poetry show --top-level command. + * Builds poetry-specific show command and returns top-level package names. + */ +export class PoetryShowTopLevelCommand extends ListDirectNamesCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['show', '--no-ansi', '--top-level']; + } + + async execute(): Promise { + const args = this.buildCommand(); + const output = await runPoetry(args, undefined, this.log, this.cancellationToken); + + try { + const names = output + .split('\n') + .map((line) => line.trim()) + .map((line) => line.match(/^([a-zA-Z0-9._-]+)/)?.[1] ?? '') + .filter((name) => !!name); + return names; + } catch { + return []; + } + } +} diff --git a/src/managers/poetry/commands/version.ts b/src/managers/poetry/commands/version.ts new file mode 100644 index 00000000..fe46688d --- /dev/null +++ b/src/managers/poetry/commands/version.ts @@ -0,0 +1,27 @@ +import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; +import { getPoetryVersion } from '../poetryUtils'; + +/** + * Concrete poetry version command. + * Gets poetry version and returns version string. + */ +export class PoetryVersionCommand extends VersionCommand { + constructor(options: CommandConstructorOptions) { + super(options); + } + + protected buildCommand(): string[] { + return ['--version']; + } + + async execute(): Promise { + try { + // Poetry version is obtained via getPoetryVersion utility which handles poetry --version + // We pass the pythonExecutable as the poetry path since it was set to the poetry executable + const versionString = await getPoetryVersion(this.pythonExecutable); + return versionString || ''; + } catch { + return ''; + } + } +} diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index 54845a20..db38fb69 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -1,7 +1,5 @@ import type { Pep440Version } from '@renovatebot/pep440'; import { explain as parse } from '@renovatebot/pep440'; -import * as fsapi from 'fs-extra'; -import * as path from 'path'; import { CancellationError, CancellationToken, @@ -26,10 +24,11 @@ import { } from '../../api'; import { spawnProcess } from '../../common/childProcess.apis'; import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis'; -import { normalizePackageName } from '../builtin/utils'; +import { normalizePackageName, parsePackageSpecs } from '../builtin/utils'; import { updatePackagesAndNotify } from '../common/packageChanges'; +import { PoetryAddCommand, PoetryRemoveCommand, PoetryShowCommand, PoetryShowTopLevelCommand, PoetryVersionCommand } from './commands/index'; import { PoetryManager } from './poetryManager'; -import { getPoetry, getPoetryVersion } from './poetryUtils'; +import { getPoetry } from './poetryUtils'; export class PoetryPackageManager implements PackageManager, Disposable { private readonly _onDidChangePackages = new EventEmitter(); @@ -156,8 +155,13 @@ export class PoetryPackageManager implements PackageManager, Disposable { if (!poetry) { return undefined; } - const versionStr = await getPoetryVersion(poetry); - return versionStr ? (parse(versionStr) ?? undefined) : undefined; + const versionCmd = new PoetryVersionCommand({ + pythonExecutable: poetry, + log: this.log, + cancellationToken: undefined, + }); + const versionString = await versionCmd.execute(); + return versionString ? (parse(versionString) ?? undefined) : undefined; } async getPackageAvailableVersions( @@ -195,28 +199,24 @@ export class PoetryPackageManager implements PackageManager, Disposable { // Handle uninstalls first if (options.uninstall && options.uninstall.length > 0) { - try { - const args = ['remove', ...options.uninstall]; - this.log.info(`Running: poetry ${args.join(' ')}`); - const result = await runPoetry(args, undefined, this.log, token); - this.log.info(result); - } catch (err) { - this.log.error(`Error removing packages with Poetry: ${err}`); - throw err; - } + const removeCmd = new PoetryRemoveCommand({ + pythonExecutable: poetry, + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(options.uninstall); + await removeCmd.execute(packages); } // Handle installs if (options.install && options.install.length > 0) { - try { - const args = ['add', ...options.install]; - this.log.info(`Running: poetry ${args.join(' ')}`); - const result = await runPoetry(args, undefined, this.log, token); - this.log.info(result); - } catch (err) { - this.log.error(`Error adding packages with Poetry: ${err}`); - throw err; - } + const addCmd = new PoetryAddCommand({ + pythonExecutable: poetry, + log: this.log, + cancellationToken: token, + }); + const packages = parsePackageSpecs(options.install); + await addCmd.execute(packages); } } @@ -230,78 +230,28 @@ export class PoetryPackageManager implements PackageManager, Disposable { ); } - let cwd = process.cwd(); - const projects = this.api.getPythonProjects(); - if (projects.length === 1) { - const stat = await fsapi.stat(projects[0].uri.fsPath); - if (stat.isDirectory()) { - cwd = projects[0].uri.fsPath; - } else { - cwd = path.dirname(projects[0].uri.fsPath); - } - } else if (projects.length > 1) { - const dirs = new Set(); - await Promise.all( - projects.map(async (project) => { - const e = await this.api.getEnvironment(project.uri); - if (e?.envId.id === environment.envId.id) { - const stat = await fsapi.stat(projects[0].uri.fsPath); - const dir = stat.isDirectory() ? projects[0].uri.fsPath : path.dirname(projects[0].uri.fsPath); - if (dirs.has(dir)) { - dirs.add(dir); - } - } - }), - ); - if (dirs.size > 0) { - // ensure we have the deepest directory node picked - cwd = Array.from(dirs.values()).sort((a, b) => (a.length - b.length) * -1)[0]; - } - } - - const poetryPackages: { name: string; version: string; displayName: string; description: string }[] = []; - - try { - this.log.info(`Running: ${await getPoetry()} show --no-ansi`); - const result = await runPoetry(['show', '--no-ansi'], cwd, this.log); - - // Parse poetry show output - // Format: name version description - const lines = result.split('\n'); - for (const line of lines) { - // Updated regex to properly handle lines with the format: - // "package (!) version description" - const match = line.match(/^(\S+)(?:\s+\([!]\))?\s+(\S+)\s+(.*)/); - if (match) { - const [, name, version, description] = match; - poetryPackages.push({ - name, - version, - displayName: name, - description: `${version} - ${description?.trim() || ''}`, - }); - } - } - } catch (err) { - this.log.error(`Error refreshing packages with Poetry: ${err}`); - // Return empty array instead of throwing to avoid breaking the UI - return []; - } - - // Convert to Package objects using the API - return poetryPackages.map((pkg) => this.api.createPackageItem(pkg, environment, this)); + const showCmd = new PoetryShowCommand({ + pythonExecutable: poetry, + log: this.log, + cancellationToken: undefined, + }); + const data = await showCmd.execute(); + return (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); } async getDirectPackageNames(_environment: PythonEnvironment): Promise | undefined> { try { - const topLevelResult = await runPoetry(['show', '--no-ansi', '--top-level'], undefined, this.log); - const names = topLevelResult - .split('\n') - .map((line) => line.trim()) - .map((line) => line.match(/^([a-zA-Z0-9._-]+)/)?.[1] ?? '') - .filter((name) => !!name) - .map(normalizePackageName); - return new Set(names); + const poetry = await getPoetry(); + if (!poetry) { + return undefined; + } + const showTopLevelCmd = new PoetryShowTopLevelCommand({ + pythonExecutable: poetry, + log: this.log, + cancellationToken: undefined, + }); + const names = await showTopLevelCmd.execute(); + return names ? new Set(names.map(normalizePackageName)) : undefined; } catch (err) { this.log.error(`Error fetching direct package names with Poetry: ${err}`); return undefined; From 15394edaf662da89a89cf700c5c5274addda275d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:07:42 +0200 Subject: [PATCH 09/28] Add documentation comments to Poetry command classes with parsed commands Add comprehensive JSDoc comments to all Poetry command classes that include: - Parsed Command: The exact CLI command structure with arguments - Official Documentation: Links to Poetry CLI documentation - Description: What each command does and notable flags Commands documented: - PoetryAddCommand: poetry add [--allow-prereleases] - PoetryRemoveCommand: poetry remove - PoetryShowCommand: poetry show --no-ansi - PoetryVersionCommand: poetry --version - PoetryShowTopLevelCommand: poetry show --no-ansi --top-level This enables identification of false/invalid commands and provides reference documentation for maintainers. --- src/managers/poetry/commands/add.ts | 8 ++++++++ src/managers/poetry/commands/remove.ts | 8 ++++++++ src/managers/poetry/commands/show.ts | 8 ++++++++ src/managers/poetry/commands/showTopLevel.ts | 9 +++++++++ src/managers/poetry/commands/version.ts | 8 ++++++++ src/managers/poetry/poetryPackageManager.ts | 8 +++++++- 6 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts index 2d79371b..978574ed 100644 --- a/src/managers/poetry/commands/add.ts +++ b/src/managers/poetry/commands/add.ts @@ -2,6 +2,14 @@ import { CommandConstructorOptions, InstallCommand } from '../../base/commands/i import { runPoetry } from '../poetryPackageManager'; /** + * Poetry add command. + * + * Parsed Command: `poetry add [--allow-prereleases] [ ...]` + * + * Official Documentation: https://python-poetry.org/docs/cli/#add + * The `poetry add` command adds required packages to your pyproject.toml and installs them. + * It's the primary way to add dependencies to a Poetry project. + * * Ephemeral arguments for poetry add command (change per execution). */ interface AddEphemeralArgs { diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts index b66ea0f5..b1e87af3 100644 --- a/src/managers/poetry/commands/remove.ts +++ b/src/managers/poetry/commands/remove.ts @@ -2,6 +2,14 @@ import { CommandConstructorOptions, UninstallCommand } from '../../base/commands import { runPoetry } from '../poetryPackageManager'; /** + * Poetry remove command. + * + * Parsed Command: `poetry remove [ ...]` + * + * Official Documentation: https://python-poetry.org/docs/cli/#remove + * The `poetry remove` command removes packages from your pyproject.toml and uninstalls them + * from your virtual environment. It removes both the dependency declaration and the installed package. + * * Ephemeral arguments for poetry remove command (change per execution). */ interface RemoveEphemeralArgs { diff --git a/src/managers/poetry/commands/show.ts b/src/managers/poetry/commands/show.ts index ca729643..8173de38 100644 --- a/src/managers/poetry/commands/show.ts +++ b/src/managers/poetry/commands/show.ts @@ -3,6 +3,14 @@ import { CommandConstructorOptions, ListCommand } from '../../base/commands/inde import { runPoetry } from '../poetryPackageManager'; /** + * Poetry show command. + * + * Parsed Command: `poetry show --no-ansi` + * + * Official Documentation: https://python-poetry.org/docs/cli/#show + * The `poetry show` command displays information about the installed packages. + * The `--no-ansi` flag disables ANSI color output for easier parsing. + * * Concrete poetry show command. * Builds poetry-specific show arguments and returns PackageInfo[]. */ diff --git a/src/managers/poetry/commands/showTopLevel.ts b/src/managers/poetry/commands/showTopLevel.ts index 58783acf..2d628c25 100644 --- a/src/managers/poetry/commands/showTopLevel.ts +++ b/src/managers/poetry/commands/showTopLevel.ts @@ -2,6 +2,15 @@ import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/co import { runPoetry } from '../poetryPackageManager'; /** + * Poetry show --top-level command. + * + * Parsed Command: `poetry show --no-ansi --top-level` + * + * Official Documentation: https://python-poetry.org/docs/cli/#show + * The `poetry show` command with `--top-level` flag displays only the top-level (directly installed) + * packages. The `--no-ansi` flag disables ANSI color output for easier parsing. + * This is useful for determining which packages were explicitly installed vs. dependencies. + * * Concrete poetry show --top-level command. * Builds poetry-specific show command and returns top-level package names. */ diff --git a/src/managers/poetry/commands/version.ts b/src/managers/poetry/commands/version.ts index fe46688d..86f9c15c 100644 --- a/src/managers/poetry/commands/version.ts +++ b/src/managers/poetry/commands/version.ts @@ -2,6 +2,14 @@ import { CommandConstructorOptions, VersionCommand } from '../../base/commands/i import { getPoetryVersion } from '../poetryUtils'; /** + * Poetry version command. + * + * Parsed Command: `poetry --version` + * + * Official Documentation: https://python-poetry.org/docs/cli/#options + * The `--version` option displays the current version of Poetry. + * Returns output in format: "Poetry (version X.Y.Z)" which is parsed to extract the version string. + * * Concrete poetry version command. * Gets poetry version and returns version string. */ diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index db38fb69..0ef454e2 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -26,7 +26,13 @@ import { spawnProcess } from '../../common/childProcess.apis'; import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis'; import { normalizePackageName, parsePackageSpecs } from '../builtin/utils'; import { updatePackagesAndNotify } from '../common/packageChanges'; -import { PoetryAddCommand, PoetryRemoveCommand, PoetryShowCommand, PoetryShowTopLevelCommand, PoetryVersionCommand } from './commands/index'; +import { + PoetryAddCommand, + PoetryRemoveCommand, + PoetryShowCommand, + PoetryShowTopLevelCommand, + PoetryVersionCommand, +} from './commands/index'; import { PoetryManager } from './poetryManager'; import { getPoetry } from './poetryUtils'; From 47ad07ef010253c63f8a61f1ac86561653f223c0 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:08:26 +0200 Subject: [PATCH 10/28] Move JSDoc comments from interfaces to command classes Move the Poetry command documentation (parsed command, official documentation, and description) from the ephemeral argument interfaces to the command classes where they are more visible and semantically appropriate. --- src/managers/poetry/commands/add.ts | 17 +++++++---------- src/managers/poetry/commands/remove.ts | 17 +++++++---------- src/managers/poetry/commands/show.ts | 3 --- src/managers/poetry/commands/showTopLevel.ts | 3 --- src/managers/poetry/commands/version.ts | 3 --- 5 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts index 978574ed..ef4bb6bb 100644 --- a/src/managers/poetry/commands/add.ts +++ b/src/managers/poetry/commands/add.ts @@ -2,14 +2,6 @@ import { CommandConstructorOptions, InstallCommand } from '../../base/commands/i import { runPoetry } from '../poetryPackageManager'; /** - * Poetry add command. - * - * Parsed Command: `poetry add [--allow-prereleases] [ ...]` - * - * Official Documentation: https://python-poetry.org/docs/cli/#add - * The `poetry add` command adds required packages to your pyproject.toml and installs them. - * It's the primary way to add dependencies to a Poetry project. - * * Ephemeral arguments for poetry add command (change per execution). */ interface AddEphemeralArgs { @@ -18,8 +10,13 @@ interface AddEphemeralArgs { } /** - * Concrete poetry add command. - * Builds poetry-specific add arguments and executes via runPoetry. + * Poetry add command. + * + * Parsed Command: `poetry add [--allow-prereleases] [ ...]` + * + * Official Documentation: https://python-poetry.org/docs/cli/#add + * The `poetry add` command adds required packages to your pyproject.toml and installs them. + * It's the primary way to add dependencies to a Poetry project. */ export class PoetryAddCommand extends InstallCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts index b1e87af3..51ffeb3d 100644 --- a/src/managers/poetry/commands/remove.ts +++ b/src/managers/poetry/commands/remove.ts @@ -2,14 +2,6 @@ import { CommandConstructorOptions, UninstallCommand } from '../../base/commands import { runPoetry } from '../poetryPackageManager'; /** - * Poetry remove command. - * - * Parsed Command: `poetry remove [ ...]` - * - * Official Documentation: https://python-poetry.org/docs/cli/#remove - * The `poetry remove` command removes packages from your pyproject.toml and uninstalls them - * from your virtual environment. It removes both the dependency declaration and the installed package. - * * Ephemeral arguments for poetry remove command (change per execution). */ interface RemoveEphemeralArgs { @@ -17,8 +9,13 @@ interface RemoveEphemeralArgs { } /** - * Concrete poetry remove command. - * Builds poetry-specific remove arguments and executes via runPoetry. + * Poetry remove command. + * + * Parsed Command: `poetry remove [ ...]` + * + * Official Documentation: https://python-poetry.org/docs/cli/#remove + * The `poetry remove` command removes packages from your pyproject.toml and uninstalls them + * from your virtual environment. It removes both the dependency declaration and the installed package. */ export class PoetryRemoveCommand extends UninstallCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/poetry/commands/show.ts b/src/managers/poetry/commands/show.ts index 8173de38..227e1fb9 100644 --- a/src/managers/poetry/commands/show.ts +++ b/src/managers/poetry/commands/show.ts @@ -10,9 +10,6 @@ import { runPoetry } from '../poetryPackageManager'; * Official Documentation: https://python-poetry.org/docs/cli/#show * The `poetry show` command displays information about the installed packages. * The `--no-ansi` flag disables ANSI color output for easier parsing. - * - * Concrete poetry show command. - * Builds poetry-specific show arguments and returns PackageInfo[]. */ export class PoetryShowCommand extends ListCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/poetry/commands/showTopLevel.ts b/src/managers/poetry/commands/showTopLevel.ts index 2d628c25..14285716 100644 --- a/src/managers/poetry/commands/showTopLevel.ts +++ b/src/managers/poetry/commands/showTopLevel.ts @@ -10,9 +10,6 @@ import { runPoetry } from '../poetryPackageManager'; * The `poetry show` command with `--top-level` flag displays only the top-level (directly installed) * packages. The `--no-ansi` flag disables ANSI color output for easier parsing. * This is useful for determining which packages were explicitly installed vs. dependencies. - * - * Concrete poetry show --top-level command. - * Builds poetry-specific show command and returns top-level package names. */ export class PoetryShowTopLevelCommand extends ListDirectNamesCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/poetry/commands/version.ts b/src/managers/poetry/commands/version.ts index 86f9c15c..772fbb8e 100644 --- a/src/managers/poetry/commands/version.ts +++ b/src/managers/poetry/commands/version.ts @@ -9,9 +9,6 @@ import { getPoetryVersion } from '../poetryUtils'; * Official Documentation: https://python-poetry.org/docs/cli/#options * The `--version` option displays the current version of Poetry. * Returns output in format: "Poetry (version X.Y.Z)" which is parsed to extract the version string. - * - * Concrete poetry version command. - * Gets poetry version and returns version string. */ export class PoetryVersionCommand extends VersionCommand { constructor(options: CommandConstructorOptions) { From 3abf21d53ddd204a52b525adcf84a79faa64dd25 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:10:29 +0200 Subject: [PATCH 11/28] Add documentation comments to all command classes across all package managers Add comprehensive JSDoc comments to all 16 concrete command classes with: - Parsed Command: The exact CLI command structure with arguments - Official Documentation: Links to official tool documentation - Description: What each command does and notable flags/behavior Pip commands (6): - PipInstallCommand: python -m pip install - PipUninstallCommand: python -m pip uninstall -y - PipListCommand: python -m pip list --format=json - PipVersionCommand: python -m pip --version - PipAvailableVersionsCommand: python -m pip index versions --json - PipListDirectNamesCommand: python -m pip list --not-required UV commands (6): - UvInstallCommand: uv pip install --python - UvUninstallCommand: uv pip uninstall -y --python - UvListCommand: uv pip list --format=json - UvVersionCommand: uv --version - UvAvailableVersionsCommand: uv pip index versions --json - UvListDirectNamesCommand: uv pip list --not-required Conda commands (5): - CondaInstallCommand: conda install -y -c conda-forge - CondaUninstallCommand: conda remove -y - CondaListCommand: conda list -p --json - CondaVersionCommand: conda --version - CondaAvailableVersionsCommand: conda search --json Poetry commands already have documentation from previous commit. This enables identification of false/invalid commands and provides reference documentation across all package managers. --- .../builtin/commands/availableVersions.ts | 20 +++++++++++++++---- src/managers/builtin/commands/install.ts | 19 ++++++++++++++---- src/managers/builtin/commands/list.ts | 18 +++++++++++++---- .../builtin/commands/listDirectNames.ts | 20 +++++++++++++++---- src/managers/builtin/commands/uninstall.ts | 19 ++++++++++++++---- src/managers/builtin/commands/version.ts | 18 +++++++++++++---- .../conda/commands/availableVersions.ts | 11 ++++++++-- src/managers/conda/commands/install.ts | 11 ++++++++-- src/managers/conda/commands/list.ts | 10 ++++++++-- src/managers/conda/commands/uninstall.ts | 10 ++++++++-- src/managers/conda/commands/version.ts | 9 +++++++-- 11 files changed, 131 insertions(+), 34 deletions(-) diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 2b4cb62d..138de9ba 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -11,8 +11,14 @@ interface AvailableVersionsEphemeralArgs { } /** - * Concrete pip availableVersions command. - * Builds pip-specific availableVersions arguments, parses JSON output, and returns version strings. + * Pip available versions command. + * + * Parsed Command: `python -m pip index versions --json --python-version ` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip_index/ + * The `pip index versions` command lists all available versions of a package on PyPI. + * The `--python-version` flag filters versions compatible with the specified Python version. + * The `--json` flag outputs results in JSON format for structured parsing. */ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { constructor(options: CommandConstructorOptions) { @@ -61,8 +67,14 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { } /** - * Concrete uv availableVersions command. - * Builds uv-specific availableVersions arguments, parses JSON output, and returns version strings. + * UV available versions command. + * + * Parsed Command: `uv pip index versions --json --python-version ` + * + * Official Documentation: https://docs.astral.sh/uv/pip/ + * The `uv pip index versions` command lists all available versions of a package. + * The `--python-version` flag filters versions compatible with the specified Python version. + * The `--json` flag outputs results in JSON format for structured parsing. */ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 0ca99dd7..7b3f5128 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -12,8 +12,13 @@ interface InstallEphemeralArgs { } /** - * Concrete pip install command. - * Builds pip-specific install arguments and executes via runPython. + * Pip install command. + * + * Parsed Command: `python -m pip install [--upgrade] [--index-url ] ` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip_install/ + * The `pip install` command installs packages from the Python Package Index (PyPI). + * Supports version pinning via `package==version` syntax and index URL configuration. */ export class PipInstallCommand extends InstallCommand { private indexUrl?: string; @@ -56,8 +61,14 @@ export class PipInstallCommand extends InstallCommand { } /** - * Concrete uv install command. - * Builds uv-specific install arguments and executes via runPython. + * UV install command. + * + * Parsed Command: `uv pip install --python [--upgrade] [--index-url ] ` + * + * Official Documentation: https://docs.astral.sh/uv/pip/ + * The `uv pip install` command is UV's high-performance Python package installer. + * UV is a Rust-based pip replacement that's faster than traditional pip. + * The `--python` flag specifies the target Python interpreter. */ export class UvInstallCommand extends InstallCommand { private indexUrl?: string; diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts index 6937e47c..ade87d29 100644 --- a/src/managers/builtin/commands/list.ts +++ b/src/managers/builtin/commands/list.ts @@ -3,8 +3,13 @@ import { CommandConstructorOptions, ListCommand } from '../../base/commands/inde import { runPython } from '../helpers'; /** - * Concrete pip list command. - * Builds pip-specific list arguments, parses JSON output, and returns PackageInfo[]. + * Pip list command. + * + * Parsed Command: `python -m pip list --format=json` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip_list/ + * The `pip list` command shows all installed packages in the current environment. + * The `--format=json` flag outputs the list in JSON format for structured parsing. */ export class PipListCommand extends ListCommand { constructor(options: CommandConstructorOptions) { @@ -50,8 +55,13 @@ export class PipListCommand extends ListCommand { } /** - * Concrete uv list command. - * Builds uv-specific list arguments, parses JSON output, and returns PackageInfo[]. + * UV list command. + * + * Parsed Command: `uv pip list --format=json` + * + * Official Documentation: https://docs.astral.sh/uv/pip/ + * The `uv pip list` command shows all installed packages via UV's pip interface. + * The `--format=json` flag outputs the list in JSON format for structured parsing. */ export class UvListCommand extends ListCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts index ea6aa19b..f21bf724 100644 --- a/src/managers/builtin/commands/listDirectNames.ts +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -2,8 +2,14 @@ import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/co import { runPython } from '../helpers'; /** - * Concrete pip listDirectNames command. - * Builds pip-specific listDirectNames arguments, parses JSON output, and returns direct package names. + * Pip list direct names command. + * + * Parsed Command: `python -m pip list --format=json --not-required` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip_list/ + * The `pip list --not-required` command lists only top-level (directly installed) packages. + * Excludes transitive dependencies that are installed as requirements of other packages. + * The `--format=json` flag outputs results in JSON format for structured parsing. */ export class PipListDirectNamesCommand extends ListDirectNamesCommand { constructor(options: CommandConstructorOptions) { @@ -41,8 +47,14 @@ export class PipListDirectNamesCommand extends ListDirectNamesCommand { } /** - * Concrete uv listDirectNames command. - * Builds uv-specific listDirectNames arguments, parses JSON output, and returns direct package names. + * UV list direct names command. + * + * Parsed Command: `uv pip list --format=json --not-required` + * + * Official Documentation: https://docs.astral.sh/uv/pip/ + * The `uv pip list --not-required` command lists only top-level (directly installed) packages. + * Excludes transitive dependencies that are installed as requirements of other packages. + * The `--format=json` flag outputs results in JSON format for structured parsing. */ export class UvListDirectNamesCommand extends ListDirectNamesCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index c4a79c6c..76b50113 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -9,8 +9,13 @@ interface UninstallEphemeralArgs { } /** - * Concrete pip uninstall command. - * Builds pip-specific uninstall arguments and executes via runPython. + * Pip uninstall command. + * + * Parsed Command: `python -m pip uninstall -y ` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip_uninstall/ + * The `pip uninstall` command uninstalls installed packages from the current environment. + * The `-y` flag automatically confirms the uninstallation without prompting. */ export class PipUninstallCommand extends UninstallCommand { constructor(options: CommandConstructorOptions) { @@ -35,8 +40,14 @@ export class PipUninstallCommand extends UninstallCommand { } /** - * Concrete uv uninstall command. - * Builds uv-specific uninstall arguments and executes via runPython. + * UV uninstall command. + * + * Parsed Command: `uv pip uninstall -y --python ` + * + * Official Documentation: https://docs.astral.sh/uv/pip/ + * The `uv pip uninstall` command removes packages from the Python environment via UV. + * The `-y` flag automatically confirms uninstallation without prompting. + * The `--python` flag specifies the target Python interpreter. */ export class UvUninstallCommand extends UninstallCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index 33c3e3d7..59289e03 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -2,8 +2,13 @@ import { CommandConstructorOptions, VersionCommand } from '../../base/commands/i import { runPython } from '../helpers'; /** - * Concrete pip version command. - * Builds pip-specific version arguments, parses output, and returns version string. + * Pip version command. + * + * Parsed Command: `python -m pip --version` + * + * Official Documentation: https://pip.pypa.io/en/stable/cli/pip/ + * The `pip --version` command displays the current version of pip. + * Output format: "pip X.Y.Z from /path/to/pip (python X.Y)" */ export class PipVersionCommand extends VersionCommand { constructor(options: CommandConstructorOptions) { @@ -39,8 +44,13 @@ export class PipVersionCommand extends VersionCommand { } /** - * Concrete uv version command. - * Builds uv-specific version arguments, parses output, and returns version string. + * UV version command. + * + * Parsed Command: `uv --version` + * + * Official Documentation: https://docs.astral.sh/uv/ + * The `uv --version` command displays the current version of UV. + * Output format: "uv X.Y.Z" */ export class UvVersionCommand extends VersionCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts index 5a0b36f5..60089c70 100644 --- a/src/managers/conda/commands/availableVersions.ts +++ b/src/managers/conda/commands/availableVersions.ts @@ -9,8 +9,15 @@ interface AvailableVersionsEphemeralArgs { } /** - * Concrete conda availableVersions command. - * Builds conda-specific availableVersions arguments, parses JSON output, and returns version strings. + * Conda available versions command. + * + * Parsed Command: `conda search --json` + * + * Official Documentation: https://conda.io/projects/conda/en/latest/commands/search.html + * The `conda search` command searches for packages in the conda channels. + * The `--json` flag outputs results in JSON format for structured parsing. + * Returns all builds of all versions available; deduplication is performed in the command. + * NOTE: The pythonVersion parameter is ignored for conda (unlike pip) as conda doesn't filter by Python version. */ export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts index 23b4b6ce..244a9ba9 100644 --- a/src/managers/conda/commands/install.ts +++ b/src/managers/conda/commands/install.ts @@ -10,8 +10,15 @@ interface InstallEphemeralArgs { } /** - * Concrete conda install command. - * Builds conda-specific install arguments and executes via runConda. + * Conda install command. + * + * Parsed Command: `conda install -y -c conda-forge [--upgrade] ` + * + * Official Documentation: https://conda.io/projects/conda/en/latest/commands/install.html + * The `conda install` command installs packages in the current conda environment. + * The `-y` flag automatically confirms the installation without prompting. + * The `-c conda-forge` flag specifies the conda-forge channel as the default package source. + * The `--upgrade` flag updates packages to their newest versions. */ export class CondaInstallCommand extends InstallCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/conda/commands/list.ts b/src/managers/conda/commands/list.ts index 917ce631..4ab3b464 100644 --- a/src/managers/conda/commands/list.ts +++ b/src/managers/conda/commands/list.ts @@ -10,8 +10,14 @@ interface ListEphemeralArgs { } /** - * Concrete conda list command. - * Builds conda-specific list arguments and returns PackageInfo[]. + * Conda list command. + * + * Parsed Command: `conda list -p --json` + * + * Official Documentation: https://conda.io/projects/conda/en/latest/commands/list.html + * The `conda list` command shows all installed packages in a conda environment. + * The `-p` flag specifies the environment path (can be absolute or relative). + * The `--json` flag outputs the package list in JSON format for structured parsing. */ export class CondaListCommand extends ListCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts index 8bfc88ed..001e7993 100644 --- a/src/managers/conda/commands/uninstall.ts +++ b/src/managers/conda/commands/uninstall.ts @@ -9,8 +9,14 @@ interface UninstallEphemeralArgs { } /** - * Concrete conda uninstall command. - * Builds conda-specific uninstall arguments and executes via runConda. + * Conda uninstall command. + * + * Parsed Command: `conda remove -y ` + * + * Official Documentation: https://conda.io/projects/conda/en/latest/commands/remove.html + * The `conda remove` command (alias `conda uninstall`) removes packages from the current environment. + * The `-y` flag automatically confirms the removal without prompting. + * Removes both the package and its unused dependencies by default. */ export class CondaUninstallCommand extends UninstallCommand { constructor(options: CommandConstructorOptions) { diff --git a/src/managers/conda/commands/version.ts b/src/managers/conda/commands/version.ts index 48da6bfd..57663a26 100644 --- a/src/managers/conda/commands/version.ts +++ b/src/managers/conda/commands/version.ts @@ -2,8 +2,13 @@ import { CommandConstructorOptions, VersionCommand } from '../../base/commands/i import { runCondaExecutable } from '../condaUtils'; /** - * Concrete conda version command. - * Builds conda-specific version arguments and returns version string. + * Conda version command. + * + * Parsed Command: `conda --version` + * + * Official Documentation: https://conda.io/projects/conda/en/latest/commands.html + * The `conda --version` command displays the current version of conda. + * Output format: "conda X.Y.Z" where X.Y.Z is the semantic version. */ export class CondaVersionCommand extends VersionCommand { constructor(options: CommandConstructorOptions) { From e414ed738e46e7965752c33d08c6201526ce0feb Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:18:23 +0200 Subject: [PATCH 12/28] Refactor utils to use command classes directly instead of helper functions Replace direct helper calls (runPython, runUV) with command class usage: - refreshPipPackages() now uses PipListCommand/UvListCommand - refreshPipDirectPackageNames() now uses PipListDirectNamesCommand/UvListDirectNamesCommand Remove: - execPipList() function - no longer needed - PIP_LIST_TIMEOUT_MS constant - no longer used - parsePipListJson import - parsing now handled by command classes This eliminates intermediate abstraction layers and uses the consistent command class pattern throughout the codebase. --- src/managers/builtin/pipUtils.ts | 21 ++++++- src/managers/builtin/utils.ts | 95 +------------------------------- 2 files changed, 21 insertions(+), 95 deletions(-) diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index a2bc4a0f..8f60b081 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -12,7 +12,8 @@ import { findFiles } from '../../common/workspace.apis'; import { selectFromCommonPackagesToInstall, selectFromInstallableToInstall } from '../common/pickers'; import { Installable } from '../common/types'; import { mergePackages } from '../common/utils'; -import { refreshPipPackages } from './utils'; +import { PipListCommand, UvListCommand } from './commands/index'; +import { shouldUseUv } from './helpers'; export interface PyprojectToml { project?: { @@ -250,7 +251,23 @@ export async function getWorkspacePackagesToInstall( let common = await getCommonPackages(); let installed: string[] | undefined; if (environment) { - installed = (await refreshPipPackages(environment, log, { showProgress: true }))?.map((pkg) => pkg.name); + const pythonExecutable = environment.execInfo?.run?.executable; + if (pythonExecutable) { + const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); + const ListCmd = useUv ? UvListCommand : PipListCommand; + const listCmd = new ListCmd({ + pythonExecutable, + log, + cancellationToken: undefined, + }); + const data = await withProgress( + { + location: ProgressLocation.Notification, + }, + async () => await listCmd.execute(), + ); + installed = data?.map((pkg) => pkg.name); + } common = mergePackages(common, installed ?? []); } return selectWorkspaceOrCommon(installableResult, common, !!options.showSkipOption, installed ?? []); diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 6c4be1eb..04634803 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -1,11 +1,10 @@ -import { LogOutputChannel, ProgressLocation, QuickPickItem, Uri, window } from 'vscode'; +import { LogOutputChannel, QuickPickItem, Uri, window } from 'vscode'; import { EnvironmentManager, Package, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; -import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { getExtension } from '../../common/extension.apis'; import { Common, PixiStrings, SysManagerStrings } from '../../common/localize'; import { traceInfo, traceVerbose } from '../../common/logging'; import { getGlobalPersistentState } from '../../common/persistentState'; -import { showInformationMessage, withProgress } from '../../common/window.apis'; +import { showInformationMessage } from '../../common/window.apis'; import { openExtension } from '../../common/workbenchCommands'; import { isNativeEnvInfo, @@ -14,8 +13,6 @@ import { NativePythonFinder, } from '../common/nativePythonFinder'; import { shortenVersionString, sortEnvironments } from '../common/utils'; -import { runPython, runUV, shouldUseUv } from './helpers'; -import { parsePipListJson, parseUvTree, PipPackage } from './pipListUtils'; const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code'; const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask'; @@ -186,94 +183,6 @@ export async function refreshPythons( return sortEnvironments(collection); } -const PIP_LIST_TIMEOUT_MS = 30_000; - -async function execPipList(environment: PythonEnvironment, log?: LogOutputChannel, args?: string[]): Promise { - // Use environmentPath directly for consistency with UV environment tracking - const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); - if (useUv) { - return await runUV( - ['pip', 'list', '--python', environment.execInfo.run.executable, '--format=json', ...(args ?? [])], - undefined, - log, - undefined, - PIP_LIST_TIMEOUT_MS, - ); - } - try { - return await runPython( - environment.execInfo.run.executable, - ['-m', 'pip', 'list', '--format=json', ...(args ?? [])], - undefined, - log, - undefined, - PIP_LIST_TIMEOUT_MS, - ); - } catch (ex) { - log?.error('Error running pip list', ex); - log?.info( - 'Package list retrieval attempted using pip, action can be done with uv if installed and setting `alwaysUseUv` is enabled.', - ); - throw ex; - } -} - -export async function refreshPipPackages( - environment: PythonEnvironment, - log?: LogOutputChannel, - options?: { showProgress: boolean }, -): Promise { - let data: string; - try { - if (options?.showProgress) { - data = await withProgress( - { - location: ProgressLocation.Notification, - }, - async () => { - return await execPipList(environment, log); - }, - ); - } else { - data = await execPipList(environment, log); - } - - return parsePipListJson(data, log); - } catch (e) { - log?.error('Error refreshing packages', e); - showErrorMessageWithLogs(SysManagerStrings.packageRefreshError, log); - return undefined; - } -} - -/** - * Returns names of packages with no installed dependents (leaf packages). - * - * Uses `pip list --not-required` (pip) or `uv pip tree --depth=0` (uv). These report - * packages that nothing else depends on, which is a proxy for "directly installed" but - * not equivalent — e.g., `pip install flask werkzeug` will report werkzeug as having - * dependents (flask) even though the user installed it explicitly. - */ -export async function refreshPipDirectPackageNames( - environment: PythonEnvironment, - log?: LogOutputChannel, -): Promise { - const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); - if (useUv) { - const treeOutput = await runUV( - ['pip', 'tree', '--python', environment.execInfo.run.executable, '--depth=0'], - undefined, - log, - undefined, - PIP_LIST_TIMEOUT_MS, - ); - return parseUvTree(treeOutput); - } - const data = await execPipList(environment, log, ['--not-required']); - const packages = parsePipListJson(data); - return packages.map((pkg) => pkg.name); -} - /** * Process pip install arguments to correctly handle editable installs with extras * This function will combine consecutive -e arguments that represent the same package with extras From 80af5da793de0428c7ea4201671f5da84a87be1a Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:33:00 +0200 Subject: [PATCH 13/28] Expose base command classes to public API - Export PackageManagerCommand and supporting types (CommandSettings, CommandType, CommandResult, CommandConstructorOptions) - Export 6 abstract base command classes: InstallCommand, UninstallCommand, ListCommand, VersionCommand, AvailableVersionsCommand, ListDirectNamesCommand - Add comprehensive JSDoc documentation for each command class explaining purpose, usage patterns, and examples - Extensions can now implement custom package managers by extending these base classes - All 1269 unit tests passing, webpack compilation successful --- src/api.ts | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/api.ts b/src/api.ts index 1a9155b5..5147ceb0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1407,3 +1407,154 @@ export interface PythonEnvironmentApi PythonProjectApi, PythonExecutionApi, PythonEnvironmentVariablesApi {} + +// ============================================================================ +// Package Manager Command Classes (Base Classes for Extension Developers) +// ============================================================================ + +/** + * Base class for all package manager commands. + * + * This is the foundation for implementing command execution across different package managers + * (pip, UV, conda, poetry). Extensions can extend these classes to integrate with package + * manager operations or implement custom command types. + * + * @see {@link InstallCommand} + * @see {@link UninstallCommand} + * @see {@link ListCommand} + * @see {@link VersionCommand} + * @see {@link AvailableVersionsCommand} + * @see {@link ListDirectNamesCommand} + */ +export type { + CommandConstructorOptions, + CommandResult, + CommandSettings, + CommandType, + PackageManagerCommand, +} from './managers/base/commands/commandSettings'; + +/** + * Abstract base class for install command implementations. + * + * Implement this interface to support package installation in your package manager. + * The command handles executing the install operation and returning the exit code. + * + * @example + * ```typescript + * class MyInstallCommand extends InstallCommand { + * async execute(): Promise { + * // Implementation: run package install command + * return exitCode; + * } + * } + * ``` + * + * @see {@link PackageManager.manage} + */ +export type { InstallCommand } from './managers/base/commands/install'; + +/** + * Abstract base class for uninstall command implementations. + * + * Implement this interface to support package uninstallation in your package manager. + * The command handles executing the uninstall operation and returning the exit code. + * + * @example + * ```typescript + * class MyUninstallCommand extends UninstallCommand { + * async execute(): Promise { + * // Implementation: run package uninstall command + * return exitCode; + * } + * } + * ``` + * + * @see {@link PackageManager.manage} + */ +export type { UninstallCommand } from './managers/base/commands/uninstall'; + +/** + * Abstract base class for list command implementations. + * + * Implement this interface to retrieve the list of installed packages. + * The command executes and returns a collection of Package objects. + * + * @example + * ```typescript + * class MyListCommand extends ListCommand { + * async execute(): Promise { + * // Implementation: run list command and parse output + * return packages; + * } + * } + * ``` + * + * @see {@link PackageManager.getPackages} + */ +export type { ListCommand } from './managers/base/commands/list'; + +/** + * Abstract base class for version command implementations. + * + * Implement this interface to retrieve the version of the underlying package management tool. + * Returns a semantic version object. + * + * @example + * ```typescript + * class MyVersionCommand extends VersionCommand { + * async execute(): Promise { + * // Implementation: run version command and parse output + * return versionObject; + * } + * } + * ``` + * + * @see {@link PackageManager.getVersion} + */ +export type { VersionCommand } from './managers/base/commands/version'; + +/** + * Abstract base class for available versions command implementations. + * + * Implement this interface to retrieve the list of available versions for a package. + * Returns an array of semantic version objects sorted newest first. + * + * @example + * ```typescript + * class MyAvailableVersionsCommand extends AvailableVersionsCommand { + * async execute(): Promise { + * // Implementation: query PyPI or other registry for available versions + * return versions; + * } + * } + * ``` + * + * @see {@link PackageManager.getPackageAvailableVersions} + */ +export type { AvailableVersionsCommand } from './managers/base/commands/availableVersions'; + +/** + * Abstract base class for list direct names command implementations. + * + * Implement this interface to retrieve the names of direct (non-transitive) packages + * installed in an environment. Returns a set of package names. + * + * @remarks + * Most package managers cannot track user install intent. For pip, this uses + * `pip list --not-required` which returns packages with no installed dependents, + * not necessarily packages the user explicitly installed. This is a best-effort approximation. + * + * @example + * ```typescript + * class MyListDirectNamesCommand extends ListDirectNamesCommand { + * async execute(): Promise> { + * // Implementation: run command and return direct package names + * return packageNames; + * } + * } + * ``` + * + * @see {@link PackageManager.getDirectPackageNames} + */ +export type { ListDirectNamesCommand } from './managers/base/commands/listDirectNames'; From 886f18909678cad7cfea96290f5ec964a6eac8ab Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:37:45 +0200 Subject: [PATCH 14/28] Remove CommandType and CommandSettings from command classes - Remove CommandType type from commandSettings.ts - not used by command implementations - Remove CommandSettings interface from commandSettings.ts - will be added later with proper design - Remove CommandSettings property from all 6 base command classes - Remove CommandSettings initialization logic from base command class constructors - Remove all this.settings.executionTimeout references in concrete implementations - Replace with default 300000ms timeout value (hardcoded for now until CommandSettings reintroduced) - Remove CommandSettings and CommandType from public API exports in src/api.ts - All 1269 unit tests passing, webpack compilation successful --- src/api.ts | 2 -- .../base/commands/availableVersions.ts | 13 +-------- src/managers/base/commands/commandSettings.ts | 27 ------------------- src/managers/base/commands/index.ts | 2 -- src/managers/base/commands/install.ts | 13 +-------- src/managers/base/commands/list.ts | 13 +-------- src/managers/base/commands/listDirectNames.ts | 13 +-------- src/managers/base/commands/uninstall.ts | 13 +-------- src/managers/base/commands/version.ts | 13 +-------- .../builtin/commands/availableVersions.ts | 4 +-- src/managers/builtin/commands/install.ts | 4 +-- src/managers/builtin/commands/list.ts | 4 +-- .../builtin/commands/listDirectNames.ts | 4 +-- src/managers/builtin/commands/uninstall.ts | 4 +-- src/managers/builtin/commands/version.ts | 4 +-- 15 files changed, 18 insertions(+), 115 deletions(-) diff --git a/src/api.ts b/src/api.ts index 5147ceb0..f06e0230 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1429,8 +1429,6 @@ export interface PythonEnvironmentApi export type { CommandConstructorOptions, CommandResult, - CommandSettings, - CommandType, PackageManagerCommand, } from './managers/base/commands/commandSettings'; diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index d49573a7..312db313 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,23 +1,12 @@ -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for availableVersions commands. - * Loads availableVersions-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class AvailableVersionsCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.availableVersionsCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise; diff --git a/src/managers/base/commands/commandSettings.ts b/src/managers/base/commands/commandSettings.ts index 0cfd45e5..a96f785c 100644 --- a/src/managers/base/commands/commandSettings.ts +++ b/src/managers/base/commands/commandSettings.ts @@ -1,32 +1,5 @@ import { CancellationToken, LogOutputChannel } from 'vscode'; -export type CommandType = 'install' | 'uninstall' | 'list' | 'version' | 'availableVersions' | 'listDirectNames'; - -/** - * Settings that apply to a specific package manager command. - */ -export interface CommandSettings { - /** - * Timeout in milliseconds for command execution. 0 = no timeout. - */ - readonly executionTimeout: number; - - /** - * Whether to include verbose output from the package manager command. - */ - readonly verboseOutput: boolean; - - /** - * Whether to retry a failed command once before raising an error. - */ - readonly retryOnFailure: boolean; - - /** - * Maximum number of retry attempts for failed operations. - */ - readonly maxRetries: number; -} - /** * Result type for commands that parse output and return data. */ diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index ee72bbc1..b7c1cd7e 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -2,8 +2,6 @@ export { AvailableVersionsCommand } from './availableVersions'; export { CommandConstructorOptions, CommandResult, - CommandSettings, - CommandType, PackageManagerCommand, } from './commandSettings'; export { InstallCommand } from './install'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 01458ca3..50826d30 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,23 +1,12 @@ -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for install commands. - * Loads install-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class InstallCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.installCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise; diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index 4fd394a4..cc12a225 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,24 +1,13 @@ import { PackageInfo } from '../../../api'; -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for list commands. - * Loads list-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.listCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(): Promise; diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index a14c5000..61be7042 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -1,23 +1,12 @@ -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for listDirectNames commands. - * Loads listDirectNames-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListDirectNamesCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.listDirectNamesCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(): Promise; diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index 747ccd05..e54a2134 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,23 +1,12 @@ -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for uninstall commands. - * Loads uninstall-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class UninstallCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.uninstallCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(packages: { packageName: string; version?: string }[]): Promise; diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index 86beb173..6df8c9cf 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -1,23 +1,12 @@ -import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, CommandSettings, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** * Template class for version commands. - * Loads version-specific settings from VS Code configuration. * Subclasses implement concrete package-manager-specific logic. */ export abstract class VersionCommand extends PackageManagerCommand { - protected settings: CommandSettings; - constructor(options: CommandConstructorOptions) { super(options); - const config = getConfiguration('python-envs.packageManager.versionCommandArgs'); - this.settings = { - executionTimeout: config.get('executionTimeout', 300000), - verboseOutput: config.get('verboseOutput', false), - retryOnFailure: config.get('retryOnFailure', true), - maxRetries: config.get('maxRetries', 1), - }; } abstract execute(): Promise; diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 138de9ba..4404dde2 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -58,7 +58,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); @@ -115,7 +115,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 7b3f5128..55e7193e 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -55,7 +55,7 @@ export class PipInstallCommand extends InstallCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); } } @@ -105,7 +105,7 @@ export class UvInstallCommand extends InstallCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); } } diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts index ade87d29..367296dc 100644 --- a/src/managers/builtin/commands/list.ts +++ b/src/managers/builtin/commands/list.ts @@ -46,7 +46,7 @@ export class PipListCommand extends ListCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); @@ -99,7 +99,7 @@ export class UvListCommand extends ListCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts index f21bf724..9cfac8f6 100644 --- a/src/managers/builtin/commands/listDirectNames.ts +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -38,7 +38,7 @@ export class PipListDirectNamesCommand extends ListDirectNamesCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); @@ -84,7 +84,7 @@ export class UvListDirectNamesCommand extends ListDirectNamesCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index 76b50113..97ca8ee5 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -34,7 +34,7 @@ export class PipUninstallCommand extends UninstallCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); } } @@ -69,7 +69,7 @@ export class UvUninstallCommand extends UninstallCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); } } diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index 59289e03..0367a339 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -35,7 +35,7 @@ export class PipVersionCommand extends VersionCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); @@ -78,7 +78,7 @@ export class UvVersionCommand extends VersionCommand { undefined, this.log, this.cancellationToken, - this.settings.executionTimeout, + 300000, ); parser(output); From be2d1428a90cc18b20fcf1cd6019c0c947ef5f60 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:44:19 +0200 Subject: [PATCH 15/28] Add timeout and config properties to base command classes - Add protected timeout: number = 300000 property to all 6 base command classes - Add protected config property to each base class using getConfiguration() for command-specific settings - InstallCommand: 'python-envs.packageManager.installCommandArgs' - UninstallCommand: 'python-envs.packageManager.uninstallCommandArgs' - ListCommand: 'python-envs.packageManager.listCommandArgs' - VersionCommand: 'python-envs.packageManager.versionCommandArgs' - AvailableVersionsCommand: 'python-envs.packageManager.availableVersionsCommandArgs' - ListDirectNamesCommand: 'python-envs.packageManager.listDirectNamesCommandArgs' - Update all concrete command implementations to use this.timeout instead of hardcoded 300000 - Import getConfiguration in all base command classes - All 1269 unit tests passing, webpack compilation successful --- .../base/commands/availableVersions.ts | 4 ++++ src/managers/base/commands/index.ts | 6 +----- src/managers/base/commands/install.ts | 4 ++++ src/managers/base/commands/list.ts | 4 ++++ src/managers/base/commands/listDirectNames.ts | 4 ++++ src/managers/base/commands/uninstall.ts | 4 ++++ src/managers/base/commands/version.ts | 4 ++++ .../builtin/commands/availableVersions.ts | 4 ++-- src/managers/builtin/commands/install.ts | 18 ++---------------- src/managers/builtin/commands/list.ts | 4 ++-- .../builtin/commands/listDirectNames.ts | 4 ++-- src/managers/builtin/commands/uninstall.ts | 18 ++---------------- src/managers/builtin/commands/version.ts | 4 ++-- 13 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 312db313..0bdac072 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,3 +1,4 @@ +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -5,6 +6,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class AvailableVersionsCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.availableVersionsCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index b7c1cd7e..bbb45801 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,9 +1,5 @@ export { AvailableVersionsCommand } from './availableVersions'; -export { - CommandConstructorOptions, - CommandResult, - PackageManagerCommand, -} from './commandSettings'; +export { CommandConstructorOptions, CommandResult, PackageManagerCommand } from './commandSettings'; export { InstallCommand } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 50826d30..e398c593 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,3 +1,4 @@ +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -5,6 +6,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class InstallCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.installCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index cc12a225..7b413aaa 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,4 +1,5 @@ import { PackageInfo } from '../../../api'; +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -6,6 +7,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.listCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index 61be7042..f25a2a43 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -1,3 +1,4 @@ +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -5,6 +6,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListDirectNamesCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.listDirectNamesCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index e54a2134..54ac6571 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,3 +1,4 @@ +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -5,6 +6,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class UninstallCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.uninstallCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index 6df8c9cf..669038ed 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -1,3 +1,4 @@ +import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** @@ -5,6 +6,9 @@ import { CommandConstructorOptions, PackageManagerCommand } from './commandSetti * Subclasses implement concrete package-manager-specific logic. */ export abstract class VersionCommand extends PackageManagerCommand { + protected timeout: number = 300000; + protected config = getConfiguration('python-envs.packageManager.versionCommandArgs'); + constructor(options: CommandConstructorOptions) { super(options); } diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 4404dde2..2102c3bd 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -58,7 +58,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); @@ -115,7 +115,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 55e7193e..7e99d3c2 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -49,14 +49,7 @@ export class PipInstallCommand extends InstallCommand { async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - 300000, - ); + await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } } @@ -99,13 +92,6 @@ export class UvInstallCommand extends InstallCommand { async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { const args = this.buildCommand({ packages, upgrade }); - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - 300000, - ); + await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } } diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts index 367296dc..738ce0c4 100644 --- a/src/managers/builtin/commands/list.ts +++ b/src/managers/builtin/commands/list.ts @@ -46,7 +46,7 @@ export class PipListCommand extends ListCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); @@ -99,7 +99,7 @@ export class UvListCommand extends ListCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts index 9cfac8f6..d38b45bb 100644 --- a/src/managers/builtin/commands/listDirectNames.ts +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -38,7 +38,7 @@ export class PipListDirectNamesCommand extends ListDirectNamesCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); @@ -84,7 +84,7 @@ export class UvListDirectNamesCommand extends ListDirectNamesCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index 97ca8ee5..bad14872 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -28,14 +28,7 @@ export class PipUninstallCommand extends UninstallCommand { async execute(packages: { packageName: string; version?: string }[]): Promise { const args = this.buildCommand({ packages }); - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - 300000, - ); + await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } } @@ -63,13 +56,6 @@ export class UvUninstallCommand extends UninstallCommand { async execute(packages: { packageName: string; version?: string }[]): Promise { const args = this.buildCommand({ packages }); - await runPython( - this.pythonExecutable, - args, - undefined, - this.log, - this.cancellationToken, - 300000, - ); + await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } } diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index 0367a339..07342297 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -35,7 +35,7 @@ export class PipVersionCommand extends VersionCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); @@ -78,7 +78,7 @@ export class UvVersionCommand extends VersionCommand { undefined, this.log, this.cancellationToken, - 300000, + this.timeout, ); parser(output); From 6324f8e99951b954364d20607fe9184ad7cf1af2 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:47:47 +0200 Subject: [PATCH 16/28] Centralize ephemeral arguments to base command classes - Define InstallEphemeralArgs in InstallCommand base class with packages and upgrade fields - Define UninstallEphemeralArgs in UninstallCommand base class with packages field - Define AvailableVersionsEphemeralArgs in AvailableVersionsCommand base class with packageName, pythonVersion, and includePrerelease fields - Add abstract buildCommand method signature to each base class that uses the appropriate ephemeral args type - Export ephemeral argument types from src/managers/base/commands/index.ts - Remove duplicate type definitions from concrete implementations (PipInstallCommand, UvInstallCommand, PipUninstallCommand, UvUninstallCommand, PipAvailableVersionsCommand, UvAvailableVersionsCommand) - Update concrete implementations to import ephemeral argument types from base commands - Improves polymorphic design - eliminates duplication and establishes clear command type interface - All 1269 unit tests passing, webpack compilation successful --- src/managers/base/commands/availableVersions.ts | 11 +++++++++++ src/managers/base/commands/index.ts | 6 +++--- src/managers/base/commands/install.ts | 10 ++++++++++ src/managers/base/commands/uninstall.ts | 9 +++++++++ src/managers/builtin/commands/availableVersions.ts | 11 +---------- src/managers/builtin/commands/install.ts | 10 +--------- src/managers/builtin/commands/uninstall.ts | 9 +-------- 7 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 0bdac072..7512afe2 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,6 +1,15 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +/** + * Ephemeral arguments for available versions command (change per execution). + */ +export interface AvailableVersionsEphemeralArgs { + packageName: string; + pythonVersion: string; + includePrerelease?: boolean; +} + /** * Template class for availableVersions commands. * Subclasses implement concrete package-manager-specific logic. @@ -13,5 +22,7 @@ export abstract class AvailableVersionsCommand extends PackageManagerCommand { super(options); } + protected abstract buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[]; + abstract execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise; } diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index bbb45801..98f01060 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,7 +1,7 @@ -export { AvailableVersionsCommand } from './availableVersions'; +export { AvailableVersionsCommand, type AvailableVersionsEphemeralArgs } from './availableVersions'; export { CommandConstructorOptions, CommandResult, PackageManagerCommand } from './commandSettings'; -export { InstallCommand } from './install'; +export { InstallCommand, type InstallEphemeralArgs } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; -export { UninstallCommand } from './uninstall'; +export { UninstallCommand, type UninstallEphemeralArgs } from './uninstall'; export { VersionCommand } from './version'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index e398c593..82a50786 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,6 +1,14 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +/** + * Ephemeral arguments for install command (change per execution). + */ +export interface InstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; + upgrade?: boolean; +} + /** * Template class for install commands. * Subclasses implement concrete package-manager-specific logic. @@ -13,5 +21,7 @@ export abstract class InstallCommand extends PackageManagerCommand { super(options); } + protected abstract buildCommand(ephemeralArgs: InstallEphemeralArgs): string[]; + abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise; } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index 54ac6571..aca7bf1d 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,6 +1,13 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +/** + * Ephemeral arguments for uninstall command (change per execution). + */ +export interface UninstallEphemeralArgs { + packages: { packageName: string; version?: string }[]; +} + /** * Template class for uninstall commands. * Subclasses implement concrete package-manager-specific logic. @@ -13,5 +20,7 @@ export abstract class UninstallCommand extends PackageManagerCommand { super(options); } + protected abstract buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[]; + abstract execute(packages: { packageName: string; version?: string }[]): Promise; } diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 2102c3bd..3e01d3fd 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -1,15 +1,6 @@ -import { AvailableVersionsCommand, CommandConstructorOptions } from '../../base/commands/index'; +import { AvailableVersionsCommand, CommandConstructorOptions, type AvailableVersionsEphemeralArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; -/** - * Ephemeral arguments for availableVersions command (change per execution). - */ -interface AvailableVersionsEphemeralArgs { - packageName: string; - pythonVersion: string; - includePrerelease?: boolean; -} - /** * Pip available versions command. * diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 7e99d3c2..040ed08d 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -1,16 +1,8 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; import { processEditableInstallArgs } from '../utils'; -/** - * Ephemeral arguments for install command (change per execution). - */ -interface InstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; -} - /** * Pip install command. * diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index bad14872..fd2f19db 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -1,13 +1,6 @@ -import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; -/** - * Ephemeral arguments for uninstall command (change per execution). - */ -interface UninstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; -} - /** * Pip uninstall command. * From b7a2c3d427d21b6cffdeeb70067960aca08068b5 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:52:48 +0200 Subject: [PATCH 17/28] Make execute methods conform to ephemeral argument interfaces - Update execute method signatures in base classes to accept ephemeral args objects - InstallCommand.execute(ephemeralArgs: InstallEphemeralArgs) - UninstallCommand.execute(ephemeralArgs: UninstallEphemeralArgs) - AvailableVersionsCommand.execute(ephemeralArgs: AvailableVersionsEphemeralArgs) - Update all concrete implementations (Pip, UV, Conda, Poetry) to use ephemeral args - Remove local duplicate ephemeral arg type definitions in Conda commands - Update Poetry commands (Add, Remove) to use base class types - Update all callers to pass ephemeral args objects instead of individual parameters - pipPackageManager.ts: install, uninstall, availableVersions calls - condaPackageManager.ts: install, uninstall, availableVersions calls - poetryPackageManager.ts: add, remove calls - Improves consistency - both buildCommand and execute now use the same ephemeral args interface - All 1269 unit tests passing, webpack compilation successful --- .../base/commands/availableVersions.ts | 2 +- src/managers/base/commands/install.ts | 2 +- src/managers/base/commands/uninstall.ts | 2 +- .../builtin/commands/availableVersions.ts | 18 +++++++++++------- src/managers/builtin/commands/install.ts | 8 ++++---- src/managers/builtin/commands/uninstall.ts | 8 ++++---- src/managers/builtin/pipPackageManager.ts | 6 +++--- .../conda/commands/availableVersions.ts | 17 +++++------------ src/managers/conda/commands/install.ts | 14 +++----------- src/managers/conda/commands/uninstall.ts | 13 +++---------- src/managers/conda/condaPackageManager.ts | 6 +++--- src/managers/poetry/commands/add.ts | 16 ++++------------ src/managers/poetry/commands/remove.ts | 15 ++++----------- src/managers/poetry/poetryPackageManager.ts | 4 ++-- 14 files changed, 49 insertions(+), 82 deletions(-) diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 7512afe2..d9424027 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -24,5 +24,5 @@ export abstract class AvailableVersionsCommand extends PackageManagerCommand { protected abstract buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[]; - abstract execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise; + abstract execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise; } diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 82a50786..6b4644b9 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -23,5 +23,5 @@ export abstract class InstallCommand extends PackageManagerCommand { protected abstract buildCommand(ephemeralArgs: InstallEphemeralArgs): string[]; - abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise; + abstract execute(ephemeralArgs: InstallEphemeralArgs): Promise; } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index aca7bf1d..eb2aacb4 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -22,5 +22,5 @@ export abstract class UninstallCommand extends PackageManagerCommand { protected abstract buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[]; - abstract execute(packages: { packageName: string; version?: string }[]): Promise; + abstract execute(ephemeralArgs: UninstallEphemeralArgs): Promise; } diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 3e01d3fd..458edfef 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -1,4 +1,8 @@ -import { AvailableVersionsCommand, CommandConstructorOptions, type AvailableVersionsEphemeralArgs } from '../../base/commands/index'; +import { + AvailableVersionsCommand, + CommandConstructorOptions, + type AvailableVersionsEphemeralArgs, +} from '../../base/commands/index'; import { runPython } from '../helpers'; /** @@ -20,7 +24,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { return ['-m', 'pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; } - async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { + async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { let availableVersions: string[] = []; const parser = (output: string): void => { @@ -32,7 +36,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { try { const parsed = JSON.parse(match[0]) as { versions?: string[] }; let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; - if (!includePrerelease) { + if (!ephemeralArgs.includePrerelease) { versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); } availableVersions = versions; @@ -41,7 +45,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { } }; - const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + const args = this.buildCommand(ephemeralArgs); const output = await runPython( this.pythonExecutable, @@ -77,7 +81,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { return ['pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; } - async execute(packageName: string, pythonVersion: string, includePrerelease?: boolean): Promise { + async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { let availableVersions: string[] = []; const parser = (output: string): void => { @@ -89,7 +93,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { try { const parsed = JSON.parse(match[0]) as { versions?: string[] }; let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; - if (!includePrerelease) { + if (!ephemeralArgs.includePrerelease) { versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); } availableVersions = versions; @@ -98,7 +102,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { } }; - const args = this.buildCommand({ packageName, pythonVersion, includePrerelease }); + const args = this.buildCommand(ephemeralArgs); const output = await runPython( this.pythonExecutable, diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 040ed08d..60dffadf 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -38,8 +38,8 @@ export class PipInstallCommand extends InstallCommand { return args; } - async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { - const args = this.buildCommand({ packages, upgrade }); + async execute(ephemeralArgs: InstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } @@ -81,8 +81,8 @@ export class UvInstallCommand extends InstallCommand { return args; } - async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { - const args = this.buildCommand({ packages, upgrade }); + async execute(ephemeralArgs: InstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index fd2f19db..d3045303 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -18,8 +18,8 @@ export class PipUninstallCommand extends UninstallCommand { return ['-m', 'pip', 'uninstall', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); + async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } @@ -46,8 +46,8 @@ export class UvUninstallCommand extends UninstallCommand { return args; } - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); + async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index e1b9e9c3..4be35ce0 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -104,7 +104,7 @@ export class PipPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute(packages); + await uninstallCmd.execute({ packages }); } // Execute install if needed @@ -116,7 +116,7 @@ export class PipPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(toInstall); - await installCmd.execute(packages, options.upgrade); + await installCmd.execute({ packages, upgrade: options.upgrade }); } await updatePackagesAndNotify( @@ -238,7 +238,7 @@ export class PipPackageManager implements PackageManager, Disposable { } } - const versionStrings = await availableVersionsCmd.execute(packageName, environment.version); + const versionStrings = await availableVersionsCmd.execute({ packageName, pythonVersion: environment.version }); return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts index 60089c70..b2f6fa0c 100644 --- a/src/managers/conda/commands/availableVersions.ts +++ b/src/managers/conda/commands/availableVersions.ts @@ -1,13 +1,6 @@ -import { AvailableVersionsCommand, CommandConstructorOptions } from '../../base/commands/index'; +import { AvailableVersionsCommand, CommandConstructorOptions, type AvailableVersionsEphemeralArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; -/** - * Ephemeral arguments for conda availableVersions command (change per execution). - */ -interface AvailableVersionsEphemeralArgs { - packageName: string; -} - /** * Conda available versions command. * @@ -28,15 +21,15 @@ export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { return ['search', ephemeralArgs.packageName, '--json']; } - async execute(packageName: string, _pythonVersion: string, _includePrerelease?: boolean): Promise { - const args = this.buildCommand({ packageName }); + async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); const output = await runCondaExecutable(args, this.log, this.cancellationToken); try { const parsed = JSON.parse(output); - if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { + if (parsed && typeof parsed === 'object' && Array.isArray(parsed[ephemeralArgs.packageName])) { const uniqueVersions = new Map(); - (parsed[packageName] as Array<{ version?: string }>) + (parsed[ephemeralArgs.packageName] as Array<{ version?: string }>) .filter((entry) => !!entry.version?.trim()) .forEach((entry) => { const version = entry.version!.trim(); diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts index 244a9ba9..06c32b7e 100644 --- a/src/managers/conda/commands/install.ts +++ b/src/managers/conda/commands/install.ts @@ -1,14 +1,6 @@ -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; -/** - * Ephemeral arguments for install command (change per execution). - */ -interface InstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; -} - /** * Conda install command. * @@ -44,8 +36,8 @@ export class CondaInstallCommand extends InstallCommand { return args; } - async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { - const args = this.buildCommand({ packages, upgrade }); + async execute(ephemeralArgs: InstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runCondaExecutable(args, this.log, this.cancellationToken); } diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts index 001e7993..685b0728 100644 --- a/src/managers/conda/commands/uninstall.ts +++ b/src/managers/conda/commands/uninstall.ts @@ -1,13 +1,6 @@ -import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; -/** - * Ephemeral arguments for uninstall command (change per execution). - */ -interface UninstallEphemeralArgs { - packages: { packageName: string; version?: string }[]; -} - /** * Conda uninstall command. * @@ -27,8 +20,8 @@ export class CondaUninstallCommand extends UninstallCommand { return ['remove', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); + async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runCondaExecutable(args, this.log, this.cancellationToken); } diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 6935b01a..67a7e839 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -86,7 +86,7 @@ export class CondaPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute(packages); + await uninstallCmd.execute({ packages }); } // Execute install if needed @@ -97,7 +97,7 @@ export class CondaPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(toInstall); - await installCmd.execute(packages, options.upgrade); + await installCmd.execute({ packages, upgrade: options.upgrade }); } await updatePackagesAndNotify( @@ -185,7 +185,7 @@ export class CondaPackageManager implements PackageManager, Disposable { log: this.log, cancellationToken: undefined, }); - const versionStrings = await availableVersionsCmd.execute(packageName, ''); + const versionStrings = await availableVersionsCmd.execute({ packageName, pythonVersion: '' }); return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts index ef4bb6bb..24767cd0 100644 --- a/src/managers/poetry/commands/add.ts +++ b/src/managers/poetry/commands/add.ts @@ -1,14 +1,6 @@ -import { CommandConstructorOptions, InstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; -/** - * Ephemeral arguments for poetry add command (change per execution). - */ -interface AddEphemeralArgs { - packages: { packageName: string; version?: string }[]; - upgrade?: boolean; -} - /** * Poetry add command. * @@ -23,7 +15,7 @@ export class PoetryAddCommand extends InstallCommand { super(options); } - protected buildCommand(ephemeralArgs: AddEphemeralArgs): string[] { + protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { const args = ['add']; if (ephemeralArgs.upgrade) { @@ -42,8 +34,8 @@ export class PoetryAddCommand extends InstallCommand { return args; } - async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise { - const args = this.buildCommand({ packages, upgrade }); + async execute(ephemeralArgs: InstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPoetry(args, undefined, this.log, this.cancellationToken); } } diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts index 51ffeb3d..67f599e0 100644 --- a/src/managers/poetry/commands/remove.ts +++ b/src/managers/poetry/commands/remove.ts @@ -1,13 +1,6 @@ -import { CommandConstructorOptions, UninstallCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; -/** - * Ephemeral arguments for poetry remove command (change per execution). - */ -interface RemoveEphemeralArgs { - packages: { packageName: string; version?: string }[]; -} - /** * Poetry remove command. * @@ -22,12 +15,12 @@ export class PoetryRemoveCommand extends UninstallCommand { super(options); } - protected buildCommand(ephemeralArgs: RemoveEphemeralArgs): string[] { + protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { return ['remove', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(packages: { packageName: string; version?: string }[]): Promise { - const args = this.buildCommand({ packages }); + async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { + const args = this.buildCommand(ephemeralArgs); await runPoetry(args, undefined, this.log, this.cancellationToken); } } diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index 0ef454e2..3593931b 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -211,7 +211,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(options.uninstall); - await removeCmd.execute(packages); + await removeCmd.execute({ packages }); } // Handle installs @@ -222,7 +222,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { cancellationToken: token, }); const packages = parsePackageSpecs(options.install); - await addCmd.execute(packages); + await addCmd.execute({ packages }); } } From e7fdf046a6f1e20cd61cdd545569bd19a23feb86 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:53:47 +0200 Subject: [PATCH 18/28] Lint --- src/managers/builtin/pipPackageManager.ts | 5 ++++- src/managers/conda/commands/availableVersions.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index 4be35ce0..cc5bea7f 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -238,7 +238,10 @@ export class PipPackageManager implements PackageManager, Disposable { } } - const versionStrings = await availableVersionsCmd.execute({ packageName, pythonVersion: environment.version }); + const versionStrings = await availableVersionsCmd.execute({ + packageName, + pythonVersion: environment.version, + }); return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; } catch { return undefined; diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts index b2f6fa0c..aec04768 100644 --- a/src/managers/conda/commands/availableVersions.ts +++ b/src/managers/conda/commands/availableVersions.ts @@ -1,4 +1,8 @@ -import { AvailableVersionsCommand, CommandConstructorOptions, type AvailableVersionsEphemeralArgs } from '../../base/commands/index'; +import { + AvailableVersionsCommand, + CommandConstructorOptions, + type AvailableVersionsEphemeralArgs, +} from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; /** From fc00718b1676c7c7796a7683cd5f0c180cbcf68b Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 05:59:34 +0200 Subject: [PATCH 19/28] Rename ephemeralArgs to executeArgs for clarity - Rename InstallEphemeralArgs -> InstallExecuteArgs - Rename UninstallEphemeralArgs -> UninstallExecuteArgs - Rename AvailableVersionsEphemeralArgs -> AvailableVersionsExecuteArgs - Rename all ephemeralArgs parameter names to executeArgs throughout - Update all base classes, concrete implementations, and callers - Update documentation comments from 'ephemeral' to execution-focused language - All 1269 unit tests passing, webpack compilation successful - Improves naming clarity: 'executeArgs' better describes parameters passed to execute methods --- .../base/commands/availableVersions.ts | 8 +++--- src/managers/base/commands/index.ts | 6 ++--- src/managers/base/commands/install.ts | 8 +++--- src/managers/base/commands/uninstall.ts | 8 +++--- .../builtin/commands/availableVersions.ts | 26 +++++++++---------- src/managers/builtin/commands/install.ts | 22 ++++++++-------- src/managers/builtin/commands/uninstall.ts | 18 ++++++------- .../conda/commands/availableVersions.ts | 14 +++++----- src/managers/conda/commands/install.ts | 12 ++++----- src/managers/conda/commands/uninstall.ts | 10 +++---- src/managers/poetry/commands/add.ts | 12 ++++----- src/managers/poetry/commands/remove.ts | 10 +++---- 12 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index d9424027..530a4617 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -2,9 +2,9 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** - * Ephemeral arguments for available versions command (change per execution). + * Arguments for available versions command execution (change per execution). */ -export interface AvailableVersionsEphemeralArgs { +export interface AvailableVersionsExecuteArgs { packageName: string; pythonVersion: string; includePrerelease?: boolean; @@ -22,7 +22,7 @@ export abstract class AvailableVersionsCommand extends PackageManagerCommand { super(options); } - protected abstract buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[]; + protected abstract buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[]; - abstract execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise; + abstract execute(executeArgs: AvailableVersionsExecuteArgs): Promise; } diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index 98f01060..711160f6 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,7 +1,7 @@ -export { AvailableVersionsCommand, type AvailableVersionsEphemeralArgs } from './availableVersions'; +export { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from './availableVersions'; export { CommandConstructorOptions, CommandResult, PackageManagerCommand } from './commandSettings'; -export { InstallCommand, type InstallEphemeralArgs } from './install'; +export { InstallCommand, type InstallExecuteArgs } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; -export { UninstallCommand, type UninstallEphemeralArgs } from './uninstall'; +export { UninstallCommand, type UninstallExecuteArgs } from './uninstall'; export { VersionCommand } from './version'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 6b4644b9..22f38f1d 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -2,9 +2,9 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** - * Ephemeral arguments for install command (change per execution). + * Arguments for install command execution (change per execution). */ -export interface InstallEphemeralArgs { +export interface InstallExecuteArgs { packages: { packageName: string; version?: string }[]; upgrade?: boolean; } @@ -21,7 +21,7 @@ export abstract class InstallCommand extends PackageManagerCommand { super(options); } - protected abstract buildCommand(ephemeralArgs: InstallEphemeralArgs): string[]; + protected abstract buildCommand(executeArgs: InstallExecuteArgs): string[]; - abstract execute(ephemeralArgs: InstallEphemeralArgs): Promise; + abstract execute(executeArgs: InstallExecuteArgs): Promise; } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index eb2aacb4..f0d4d2ae 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -2,9 +2,9 @@ import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; /** - * Ephemeral arguments for uninstall command (change per execution). + * Arguments for uninstall command execution (change per execution). */ -export interface UninstallEphemeralArgs { +export interface UninstallExecuteArgs { packages: { packageName: string; version?: string }[]; } @@ -20,7 +20,7 @@ export abstract class UninstallCommand extends PackageManagerCommand { super(options); } - protected abstract buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[]; + protected abstract buildCommand(executeArgs: UninstallExecuteArgs): string[]; - abstract execute(ephemeralArgs: UninstallEphemeralArgs): Promise; + abstract execute(executeArgs: UninstallExecuteArgs): Promise; } diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 458edfef..8d921c59 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -1,7 +1,7 @@ import { AvailableVersionsCommand, CommandConstructorOptions, - type AvailableVersionsEphemeralArgs, + type AvailableVersionsExecuteArgs, } from '../../base/commands/index'; import { runPython } from '../helpers'; @@ -19,12 +19,12 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { constructor(options: CommandConstructorOptions) { super(options); } - protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { - const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); - return ['-m', 'pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; + protected buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[] { + const baseVersion = executeArgs.pythonVersion.split('.').slice(0, 2).join('.'); + return ['-m', 'pip', 'index', 'versions', executeArgs.packageName, '--json', '--python-version', baseVersion]; } - async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { + async execute(executeArgs: AvailableVersionsExecuteArgs): Promise { let availableVersions: string[] = []; const parser = (output: string): void => { @@ -36,7 +36,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { try { const parsed = JSON.parse(match[0]) as { versions?: string[] }; let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; - if (!ephemeralArgs.includePrerelease) { + if (!executeArgs.includePrerelease) { versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); } availableVersions = versions; @@ -45,7 +45,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { } }; - const args = this.buildCommand(ephemeralArgs); + const args = this.buildCommand(executeArgs); const output = await runPython( this.pythonExecutable, @@ -76,12 +76,12 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { super(options); } - protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { - const baseVersion = ephemeralArgs.pythonVersion.split('.').slice(0, 2).join('.'); - return ['pip', 'index', 'versions', ephemeralArgs.packageName, '--json', '--python-version', baseVersion]; + protected buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[] { + const baseVersion = executeArgs.pythonVersion.split('.').slice(0, 2).join('.'); + return ['pip', 'index', 'versions', executeArgs.packageName, '--json', '--python-version', baseVersion]; } - async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { + async execute(executeArgs: AvailableVersionsExecuteArgs): Promise { let availableVersions: string[] = []; const parser = (output: string): void => { @@ -93,7 +93,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { try { const parsed = JSON.parse(match[0]) as { versions?: string[] }; let versions = Array.isArray(parsed.versions) ? parsed.versions.filter((v) => !!v.trim()) : []; - if (!ephemeralArgs.includePrerelease) { + if (!executeArgs.includePrerelease) { versions = versions.filter((version) => !/[ab]|rc|dev/i.test(version)); } availableVersions = versions; @@ -102,7 +102,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { } }; - const args = this.buildCommand(ephemeralArgs); + const args = this.buildCommand(executeArgs); const output = await runPython( this.pythonExecutable, diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index 60dffadf..d69f7a43 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallExecuteArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; import { processEditableInstallArgs } from '../utils'; @@ -21,25 +21,25 @@ export class PipInstallCommand extends InstallCommand { this.indexUrl = config.get('indexUrl'); } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + protected buildCommand(executeArgs: InstallExecuteArgs): string[] { let args = ['-m', 'pip', 'install']; if (this.indexUrl) { args.push('--index-url', this.indexUrl); } - if (ephemeralArgs.upgrade) { + if (executeArgs.upgrade) { args.push('--upgrade'); } - const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName)); + const processedArgs = processEditableInstallArgs(executeArgs.packages.map((pkg) => pkg.packageName)); args.push(...processedArgs); return args; } - async execute(ephemeralArgs: InstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: InstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } @@ -64,25 +64,25 @@ export class UvInstallCommand extends InstallCommand { this.indexUrl = config.get('indexUrl'); } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + protected buildCommand(executeArgs: InstallExecuteArgs): string[] { let args = ['pip', 'install', '--python', this.pythonExecutable]; if (this.indexUrl) { args.push('--index-url', this.indexUrl); } - if (ephemeralArgs.upgrade) { + if (executeArgs.upgrade) { args.push('--upgrade'); } - const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName)); + const processedArgs = processEditableInstallArgs(executeArgs.packages.map((pkg) => pkg.packageName)); args.push(...processedArgs); return args; } - async execute(ephemeralArgs: InstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: InstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index d3045303..b914c736 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallExecuteArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; /** @@ -14,12 +14,12 @@ export class PipUninstallCommand extends UninstallCommand { constructor(options: CommandConstructorOptions) { super(options); } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['-m', 'pip', 'uninstall', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + protected buildCommand(executeArgs: UninstallExecuteArgs): string[] { + return ['-m', 'pip', 'uninstall', '-y', ...executeArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: UninstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } @@ -40,14 +40,14 @@ export class UvUninstallCommand extends UninstallCommand { super(options); } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { + protected buildCommand(executeArgs: UninstallExecuteArgs): string[] { const args = ['pip', 'uninstall', '-y', '--python', this.pythonExecutable]; - args.push(...ephemeralArgs.packages.map((pkg) => pkg.packageName)); + args.push(...executeArgs.packages.map((pkg) => pkg.packageName)); return args; } - async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: UninstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); } diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts index aec04768..467d304a 100644 --- a/src/managers/conda/commands/availableVersions.ts +++ b/src/managers/conda/commands/availableVersions.ts @@ -1,7 +1,7 @@ import { AvailableVersionsCommand, CommandConstructorOptions, - type AvailableVersionsEphemeralArgs, + type AvailableVersionsExecuteArgs, } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; @@ -21,19 +21,19 @@ export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { super(options); } - protected buildCommand(ephemeralArgs: AvailableVersionsEphemeralArgs): string[] { - return ['search', ephemeralArgs.packageName, '--json']; + protected buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[] { + return ['search', executeArgs.packageName, '--json']; } - async execute(ephemeralArgs: AvailableVersionsEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: AvailableVersionsExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); const output = await runCondaExecutable(args, this.log, this.cancellationToken); try { const parsed = JSON.parse(output); - if (parsed && typeof parsed === 'object' && Array.isArray(parsed[ephemeralArgs.packageName])) { + if (parsed && typeof parsed === 'object' && Array.isArray(parsed[executeArgs.packageName])) { const uniqueVersions = new Map(); - (parsed[ephemeralArgs.packageName] as Array<{ version?: string }>) + (parsed[executeArgs.packageName] as Array<{ version?: string }>) .filter((entry) => !!entry.version?.trim()) .forEach((entry) => { const version = entry.version!.trim(); diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts index 06c32b7e..34dddf0b 100644 --- a/src/managers/conda/commands/install.ts +++ b/src/managers/conda/commands/install.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallExecuteArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; /** @@ -17,15 +17,15 @@ export class CondaInstallCommand extends InstallCommand { super(options); } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + protected buildCommand(executeArgs: InstallExecuteArgs): string[] { let args = ['install', '-y', '-c', 'conda-forge']; - if (ephemeralArgs.upgrade) { + if (executeArgs.upgrade) { args.push('--upgrade'); } args.push( - ...ephemeralArgs.packages.map((pkg) => { + ...executeArgs.packages.map((pkg) => { if (pkg.version) { return `${pkg.packageName}=${pkg.version}`; } @@ -36,8 +36,8 @@ export class CondaInstallCommand extends InstallCommand { return args; } - async execute(ephemeralArgs: InstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: InstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runCondaExecutable(args, this.log, this.cancellationToken); } diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts index 685b0728..ce9ed93c 100644 --- a/src/managers/conda/commands/uninstall.ts +++ b/src/managers/conda/commands/uninstall.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallExecuteArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; /** @@ -16,12 +16,12 @@ export class CondaUninstallCommand extends UninstallCommand { super(options); } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['remove', '-y', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + protected buildCommand(executeArgs: UninstallExecuteArgs): string[] { + return ['remove', '-y', ...executeArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: UninstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runCondaExecutable(args, this.log, this.cancellationToken); } diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts index 24767cd0..fddf52ef 100644 --- a/src/managers/poetry/commands/add.ts +++ b/src/managers/poetry/commands/add.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, InstallCommand, type InstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, InstallCommand, type InstallExecuteArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; /** @@ -15,15 +15,15 @@ export class PoetryAddCommand extends InstallCommand { super(options); } - protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] { + protected buildCommand(executeArgs: InstallExecuteArgs): string[] { const args = ['add']; - if (ephemeralArgs.upgrade) { + if (executeArgs.upgrade) { args.push('--allow-prereleases'); } args.push( - ...ephemeralArgs.packages.map((pkg) => { + ...executeArgs.packages.map((pkg) => { if (pkg.version) { return `${pkg.packageName}@${pkg.version}`; } @@ -34,8 +34,8 @@ export class PoetryAddCommand extends InstallCommand { return args; } - async execute(ephemeralArgs: InstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: InstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPoetry(args, undefined, this.log, this.cancellationToken); } } diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts index 67f599e0..806901f2 100644 --- a/src/managers/poetry/commands/remove.ts +++ b/src/managers/poetry/commands/remove.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, UninstallCommand, type UninstallEphemeralArgs } from '../../base/commands/index'; +import { CommandConstructorOptions, UninstallCommand, type UninstallExecuteArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; /** @@ -15,12 +15,12 @@ export class PoetryRemoveCommand extends UninstallCommand { super(options); } - protected buildCommand(ephemeralArgs: UninstallEphemeralArgs): string[] { - return ['remove', ...ephemeralArgs.packages.map((pkg) => pkg.packageName)]; + protected buildCommand(executeArgs: UninstallExecuteArgs): string[] { + return ['remove', ...executeArgs.packages.map((pkg) => pkg.packageName)]; } - async execute(ephemeralArgs: UninstallEphemeralArgs): Promise { - const args = this.buildCommand(ephemeralArgs); + async execute(executeArgs: UninstallExecuteArgs): Promise { + const args = this.buildCommand(executeArgs); await runPoetry(args, undefined, this.log, this.cancellationToken); } } From e517a3a25dfb1b578307882a89bd13162e1e9584 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 06:03:32 +0200 Subject: [PATCH 20/28] Rename commandSettings.ts to packageManagerCommand.ts and remove unused CommandResult - Rename src/managers/base/commands/commandSettings.ts to packageManagerCommand.ts - File now named after its primary export (PackageManagerCommand) - Remove unused CommandResult interface - Not used anywhere in the codebase, only exported in API - Update all imports in base command classes to use new filename - availableVersions.ts, install.ts, list.ts, listDirectNames.ts, uninstall.ts, version.ts - Update index.ts to import from packageManagerCommand instead of commandSettings - Remove CommandResult from public API export - Delete unused src/managers/builtin/commands/commandSettings.ts file - All 1269 unit tests passing, webpack compilation successful --- src/api.ts | 3 +- .../base/commands/availableVersions.ts | 2 +- src/managers/base/commands/index.ts | 2 +- src/managers/base/commands/install.ts | 2 +- src/managers/base/commands/list.ts | 2 +- src/managers/base/commands/listDirectNames.ts | 2 +- ...ndSettings.ts => packageManagerCommand.ts} | 10 +-- src/managers/base/commands/uninstall.ts | 2 +- src/managers/base/commands/version.ts | 2 +- .../builtin/commands/commandSettings.ts | 66 ------------------- 10 files changed, 9 insertions(+), 84 deletions(-) rename src/managers/base/commands/{commandSettings.ts => packageManagerCommand.ts} (78%) delete mode 100644 src/managers/builtin/commands/commandSettings.ts diff --git a/src/api.ts b/src/api.ts index f06e0230..6bfb8a0a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1428,9 +1428,8 @@ export interface PythonEnvironmentApi */ export type { CommandConstructorOptions, - CommandResult, PackageManagerCommand, -} from './managers/base/commands/commandSettings'; +} from './managers/base/commands/packageManagerCommand'; /** * Abstract base class for install command implementations. diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 530a4617..2408b451 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for available versions command execution (change per execution). diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index 711160f6..be18674e 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,5 +1,5 @@ export { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from './availableVersions'; -export { CommandConstructorOptions, CommandResult, PackageManagerCommand } from './commandSettings'; +export { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; export { InstallCommand, type InstallExecuteArgs } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 22f38f1d..081b115f 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for install command execution (change per execution). diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index 7b413aaa..ae360b80 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,6 +1,6 @@ import { PackageInfo } from '../../../api'; import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for list commands. diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index f25a2a43..00f3714c 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for listDirectNames commands. diff --git a/src/managers/base/commands/commandSettings.ts b/src/managers/base/commands/packageManagerCommand.ts similarity index 78% rename from src/managers/base/commands/commandSettings.ts rename to src/managers/base/commands/packageManagerCommand.ts index a96f785c..a4c8fcca 100644 --- a/src/managers/base/commands/commandSettings.ts +++ b/src/managers/base/commands/packageManagerCommand.ts @@ -1,13 +1,5 @@ import { CancellationToken, LogOutputChannel } from 'vscode'; -/** - * Result type for commands that parse output and return data. - */ -export interface CommandResult { - readonly data: T; - readonly rawOutput: string; -} - /** * Constructor options shared by all package manager commands. */ @@ -35,5 +27,5 @@ export abstract class PackageManagerCommand { /** * Subclasses implement to build the command arguments. */ - protected abstract buildCommand(ephemeralArgs: unknown): string[]; + protected abstract buildCommand(executeArgs: unknown): string[]; } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index f0d4d2ae..4a917781 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for uninstall command execution (change per execution). diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index 669038ed..f785e0ee 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -1,5 +1,5 @@ import { getConfiguration } from '../../../common/workspace.apis'; -import { CommandConstructorOptions, PackageManagerCommand } from './commandSettings'; +import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for version commands. diff --git a/src/managers/builtin/commands/commandSettings.ts b/src/managers/builtin/commands/commandSettings.ts deleted file mode 100644 index 0cfd45e5..00000000 --- a/src/managers/builtin/commands/commandSettings.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { CancellationToken, LogOutputChannel } from 'vscode'; - -export type CommandType = 'install' | 'uninstall' | 'list' | 'version' | 'availableVersions' | 'listDirectNames'; - -/** - * Settings that apply to a specific package manager command. - */ -export interface CommandSettings { - /** - * Timeout in milliseconds for command execution. 0 = no timeout. - */ - readonly executionTimeout: number; - - /** - * Whether to include verbose output from the package manager command. - */ - readonly verboseOutput: boolean; - - /** - * Whether to retry a failed command once before raising an error. - */ - readonly retryOnFailure: boolean; - - /** - * Maximum number of retry attempts for failed operations. - */ - readonly maxRetries: number; -} - -/** - * Result type for commands that parse output and return data. - */ -export interface CommandResult { - readonly data: T; - readonly rawOutput: string; -} - -/** - * Constructor options shared by all package manager commands. - */ -export interface CommandConstructorOptions { - pythonExecutable: string; - log?: LogOutputChannel; - cancellationToken?: CancellationToken; -} - -/** - * Base class for all package manager commands. - * Provides common properties and minimal interface for subclasses. - */ -export abstract class PackageManagerCommand { - protected pythonExecutable: string; - protected log?: LogOutputChannel; - protected cancellationToken?: CancellationToken; - - constructor(options: CommandConstructorOptions) { - this.pythonExecutable = options.pythonExecutable; - this.log = options.log; - this.cancellationToken = options.cancellationToken; - } - - /** - * Subclasses implement to build the command arguments. - */ - protected abstract buildCommand(ephemeralArgs: unknown): string[]; -} From 55e7f789dd66e7045d95860e4c8053ddfcba620e Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 06:06:59 +0200 Subject: [PATCH 21/28] Move timeout default to PackageManagerCommand base class - Add protected timeout property with 300000ms default to PackageManagerCommand - Remove duplicate timeout declarations from all 6 command-specific classes - InstallCommand, UninstallCommand, AvailableVersionsCommand - ListCommand, VersionCommand, ListDirectNamesCommand - Subclasses can still override if needed for custom timeout behavior - Centralizes timeout configuration at the base level - All 1269 unit tests passing, webpack compilation successful --- src/api.ts | 5 +---- src/managers/base/commands/availableVersions.ts | 1 - src/managers/base/commands/index.ts | 2 +- src/managers/base/commands/install.ts | 1 - src/managers/base/commands/list.ts | 1 - src/managers/base/commands/listDirectNames.ts | 1 - src/managers/base/commands/packageManagerCommand.ts | 1 + src/managers/base/commands/uninstall.ts | 1 - src/managers/base/commands/version.ts | 1 - 9 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/api.ts b/src/api.ts index 6bfb8a0a..af802ea9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1426,10 +1426,7 @@ export interface PythonEnvironmentApi * @see {@link AvailableVersionsCommand} * @see {@link ListDirectNamesCommand} */ -export type { - CommandConstructorOptions, - PackageManagerCommand, -} from './managers/base/commands/packageManagerCommand'; +export type { CommandConstructorOptions, PackageManagerCommand } from './managers/base/commands/packageManagerCommand'; /** * Abstract base class for install command implementations. diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 2408b451..3ed463c8 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -15,7 +15,6 @@ export interface AvailableVersionsExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class AvailableVersionsCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.availableVersionsCommandArgs'); constructor(options: CommandConstructorOptions) { diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index be18674e..fd225c50 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -1,7 +1,7 @@ export { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from './availableVersions'; -export { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; export { InstallCommand, type InstallExecuteArgs } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; +export { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; export { UninstallCommand, type UninstallExecuteArgs } from './uninstall'; export { VersionCommand } from './version'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index 081b115f..f4a161bb 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -14,7 +14,6 @@ export interface InstallExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class InstallCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.installCommandArgs'); constructor(options: CommandConstructorOptions) { diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index ae360b80..13489e7c 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -7,7 +7,6 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.listCommandArgs'); constructor(options: CommandConstructorOptions) { diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index 00f3714c..842ee36f 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -6,7 +6,6 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListDirectNamesCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.listDirectNamesCommandArgs'); constructor(options: CommandConstructorOptions) { diff --git a/src/managers/base/commands/packageManagerCommand.ts b/src/managers/base/commands/packageManagerCommand.ts index a4c8fcca..9be3c53c 100644 --- a/src/managers/base/commands/packageManagerCommand.ts +++ b/src/managers/base/commands/packageManagerCommand.ts @@ -17,6 +17,7 @@ export abstract class PackageManagerCommand { protected pythonExecutable: string; protected log?: LogOutputChannel; protected cancellationToken?: CancellationToken; + protected timeout: number = 300000; constructor(options: CommandConstructorOptions) { this.pythonExecutable = options.pythonExecutable; diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index 4a917781..16c3249a 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -13,7 +13,6 @@ export interface UninstallExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class UninstallCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.uninstallCommandArgs'); constructor(options: CommandConstructorOptions) { diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index f785e0ee..cc75f617 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -6,7 +6,6 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class VersionCommand extends PackageManagerCommand { - protected timeout: number = 300000; protected config = getConfiguration('python-envs.packageManager.versionCommandArgs'); constructor(options: CommandConstructorOptions) { From e4730db64d9a64eccc0e5ddb6437f5ca59a9ed70 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 06:18:32 +0200 Subject: [PATCH 22/28] Centralize configuration loading to PackageManagerCommand base class - Add configSection optional parameter to CommandConstructorOptions - Move getConfiguration() call to PackageManagerCommand constructor - Remove duplicate config assignments from all 6 base command classes - InstallCommand, UninstallCommand, AvailableVersionsCommand - ListCommand, VersionCommand, ListDirectNamesCommand - Each derived class now passes its config section via super() call - Reduces boilerplate by 6 duplicate lines (1 per command class) - Makes adding new command types easier (no config setup needed) - Centralized pattern: python-envs.packageManager.\ - All 1269 unit tests passing, webpack compilation successful --- src/managers/base/commands/availableVersions.ts | 5 +---- src/managers/base/commands/install.ts | 5 +---- src/managers/base/commands/list.ts | 5 +---- src/managers/base/commands/listDirectNames.ts | 5 +---- src/managers/base/commands/packageManagerCommand.ts | 8 +++++++- src/managers/base/commands/uninstall.ts | 5 +---- src/managers/base/commands/version.ts | 5 +---- 7 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index 3ed463c8..ee92322c 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,4 +1,3 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -15,10 +14,8 @@ export interface AvailableVersionsExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class AvailableVersionsCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.availableVersionsCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'availableVersionsCommandArgs' }); } protected abstract buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[]; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index f4a161bb..df9574dd 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,4 +1,3 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -14,10 +13,8 @@ export interface InstallExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class InstallCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.installCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'installCommandArgs' }); } protected abstract buildCommand(executeArgs: InstallExecuteArgs): string[]; diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index 13489e7c..2f300c3e 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,5 +1,4 @@ import { PackageInfo } from '../../../api'; -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -7,10 +6,8 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.listCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'listCommandArgs' }); } abstract execute(): Promise; diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index 842ee36f..071173fa 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -1,4 +1,3 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -6,10 +5,8 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class ListDirectNamesCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.listDirectNamesCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'listDirectNamesCommandArgs' }); } abstract execute(): Promise; diff --git a/src/managers/base/commands/packageManagerCommand.ts b/src/managers/base/commands/packageManagerCommand.ts index 9be3c53c..00977288 100644 --- a/src/managers/base/commands/packageManagerCommand.ts +++ b/src/managers/base/commands/packageManagerCommand.ts @@ -1,10 +1,12 @@ -import { CancellationToken, LogOutputChannel } from 'vscode'; +import { CancellationToken, LogOutputChannel, WorkspaceConfiguration } from 'vscode'; +import { getConfiguration } from '../../../common/workspace.apis'; /** * Constructor options shared by all package manager commands. */ export interface CommandConstructorOptions { pythonExecutable: string; + configSection?: string; log?: LogOutputChannel; cancellationToken?: CancellationToken; } @@ -18,11 +20,15 @@ export abstract class PackageManagerCommand { protected log?: LogOutputChannel; protected cancellationToken?: CancellationToken; protected timeout: number = 300000; + protected config: WorkspaceConfiguration; constructor(options: CommandConstructorOptions) { this.pythonExecutable = options.pythonExecutable; this.log = options.log; this.cancellationToken = options.cancellationToken; + this.config = options.configSection + ? getConfiguration(`python-envs.packageManager.${options.configSection}`) + : getConfiguration('python-envs.packageManager'); } /** diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index 16c3249a..413d672a 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,4 +1,3 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -13,10 +12,8 @@ export interface UninstallExecuteArgs { * Subclasses implement concrete package-manager-specific logic. */ export abstract class UninstallCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.uninstallCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'uninstallCommandArgs' }); } protected abstract buildCommand(executeArgs: UninstallExecuteArgs): string[]; diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index cc75f617..9fee8a20 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -1,4 +1,3 @@ -import { getConfiguration } from '../../../common/workspace.apis'; import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** @@ -6,10 +5,8 @@ import { CommandConstructorOptions, PackageManagerCommand } from './packageManag * Subclasses implement concrete package-manager-specific logic. */ export abstract class VersionCommand extends PackageManagerCommand { - protected config = getConfiguration('python-envs.packageManager.versionCommandArgs'); - constructor(options: CommandConstructorOptions) { - super(options); + super({ ...options, configSection: 'versionCommandArgs' }); } abstract execute(): Promise; From 44756b181246bcc0a551775c46625bed442d5a22 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 06:31:55 +0200 Subject: [PATCH 23/28] Centralize CommandConstructorOptions at method level for install/uninstall - Add manageCommandOptions variable in manage() and runPoetryManage() methods - Consolidate pythonExecutable, log, and cancellationToken in single object - Reuse manageCommandOptions for both uninstall and install command creation - Eliminates repeated object literal duplication in install/uninstall blocks - Applied to: PipPackageManager, CondaPackageManager, PoetryPackageManager - Import CommandConstructorOptions type in all three managers - Clear variable name (manageCommandOptions) indicates scope and purpose - All 1269 unit tests passing, webpack compilation successful --- src/managers/builtin/pipPackageManager.ts | 20 ++++++++++---------- src/managers/conda/condaPackageManager.ts | 20 ++++++++++---------- src/managers/poetry/poetryPackageManager.ts | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index cc5bea7f..5a4f1670 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -12,6 +12,7 @@ import { window, } from 'vscode'; import { + CommandConstructorOptions, DidChangePackagesEventArgs, GetPackagesOptions, IconPath, @@ -95,14 +96,17 @@ export class PipPackageManager implements PackageManager, Disposable { // Detect whether to use UV const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + // Centralize command options for install/uninstall operations + const manageCommandOptions: CommandConstructorOptions = { + pythonExecutable, + log: this.log, + cancellationToken: token, + }; + // Execute uninstall if needed if (toUninstall.length > 0) { const UninstallCommand = useUv ? UvUninstallCommand : PipUninstallCommand; - const uninstallCmd = new UninstallCommand({ - pythonExecutable, - log: this.log, - cancellationToken: token, - }); + const uninstallCmd = new UninstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toUninstall); await uninstallCmd.execute({ packages }); } @@ -110,11 +114,7 @@ export class PipPackageManager implements PackageManager, Disposable { // Execute install if needed if (toInstall.length > 0) { const InstallCommand = useUv ? UvInstallCommand : PipInstallCommand; - const installCmd = new InstallCommand({ - pythonExecutable, - log: this.log, - cancellationToken: token, - }); + const installCmd = new InstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toInstall); await installCmd.execute({ packages, upgrade: options.upgrade }); } diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 67a7e839..1d06865c 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -12,6 +12,7 @@ import { RelativePattern, } from 'vscode'; import { + CommandConstructorOptions, DidChangePackagesEventArgs, GetPackagesOptions, IconPath, @@ -78,24 +79,23 @@ export class CondaPackageManager implements PackageManager, Disposable { }, async (_progress, token) => { try { + // Centralize command options for install/uninstall operations + const manageCommandOptions: CommandConstructorOptions = { + pythonExecutable: 'conda', + log: this.log, + cancellationToken: token, + }; + // Execute uninstall if needed if (toUninstall.length > 0) { - const uninstallCmd = new CondaUninstallCommand({ - pythonExecutable: 'conda', - log: this.log, - cancellationToken: token, - }); + const uninstallCmd = new CondaUninstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toUninstall); await uninstallCmd.execute({ packages }); } // Execute install if needed if (toInstall.length > 0) { - const installCmd = new CondaInstallCommand({ - pythonExecutable: 'conda', - log: this.log, - cancellationToken: token, - }); + const installCmd = new CondaInstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toInstall); await installCmd.execute({ packages, upgrade: options.upgrade }); } diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index 3593931b..e36eaf4a 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -13,6 +13,7 @@ import { } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { + CommandConstructorOptions, DidChangePackagesEventArgs, GetPackagesOptions, IconPath, @@ -203,24 +204,23 @@ export class PoetryPackageManager implements PackageManager, Disposable { ); } + // Centralize command options for install/uninstall operations + const manageCommandOptions: CommandConstructorOptions = { + pythonExecutable: poetry, + log: this.log, + cancellationToken: token, + }; + // Handle uninstalls first if (options.uninstall && options.uninstall.length > 0) { - const removeCmd = new PoetryRemoveCommand({ - pythonExecutable: poetry, - log: this.log, - cancellationToken: token, - }); + const removeCmd = new PoetryRemoveCommand(manageCommandOptions); const packages = parsePackageSpecs(options.uninstall); await removeCmd.execute({ packages }); } // Handle installs if (options.install && options.install.length > 0) { - const addCmd = new PoetryAddCommand({ - pythonExecutable: poetry, - log: this.log, - cancellationToken: token, - }); + const addCmd = new PoetryAddCommand(manageCommandOptions); const packages = parsePackageSpecs(options.install); await addCmd.execute({ packages }); } From 17112c6b6a193c53366ab6c293b3b0e536591d44 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 12:06:52 +0200 Subject: [PATCH 24/28] Refactor package manager command execution args and cancellation token flow --- src/api.ts | 6 ++++- .../base/commands/availableVersions.ts | 4 +-- src/managers/base/commands/index.ts | 2 +- src/managers/base/commands/install.ts | 4 +-- src/managers/base/commands/list.ts | 4 +-- src/managers/base/commands/listDirectNames.ts | 4 +-- .../base/commands/packageManagerCommand.ts | 13 +++++++--- src/managers/base/commands/uninstall.ts | 4 +-- src/managers/base/commands/version.ts | 4 +-- .../builtin/commands/availableVersions.ts | 4 +-- src/managers/builtin/commands/install.ts | 4 +-- src/managers/builtin/commands/list.ts | 10 ++++---- .../builtin/commands/listDirectNames.ts | 10 ++++---- src/managers/builtin/commands/uninstall.ts | 4 +-- src/managers/builtin/commands/version.ts | 10 ++++---- src/managers/builtin/pipPackageManager.ts | 9 ++----- src/managers/builtin/pipUtils.ts | 6 +---- .../conda/commands/availableVersions.ts | 2 +- src/managers/conda/commands/install.ts | 2 +- src/managers/conda/commands/list.ts | 20 ++++++++------- src/managers/conda/commands/uninstall.ts | 2 +- src/managers/conda/commands/version.ts | 6 ++--- src/managers/conda/condaPackageManager.ts | 10 +++----- src/managers/poetry/commands/add.ts | 2 +- src/managers/poetry/commands/remove.ts | 2 +- src/managers/poetry/commands/show.ts | 6 ++--- src/managers/poetry/commands/showTopLevel.ts | 6 ++--- src/managers/poetry/poetryPackageManager.ts | 25 ++++++++----------- 28 files changed, 89 insertions(+), 96 deletions(-) diff --git a/src/api.ts b/src/api.ts index af802ea9..90bb7939 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1426,7 +1426,11 @@ export interface PythonEnvironmentApi * @see {@link AvailableVersionsCommand} * @see {@link ListDirectNamesCommand} */ -export type { CommandConstructorOptions, PackageManagerCommand } from './managers/base/commands/packageManagerCommand'; +export type { + BaseExecuteArgs, + CommandConstructorOptions, + PackageManagerCommand, +} from './managers/base/commands/packageManagerCommand'; /** * Abstract base class for install command implementations. diff --git a/src/managers/base/commands/availableVersions.ts b/src/managers/base/commands/availableVersions.ts index ee92322c..cab9f6df 100644 --- a/src/managers/base/commands/availableVersions.ts +++ b/src/managers/base/commands/availableVersions.ts @@ -1,9 +1,9 @@ -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for available versions command execution (change per execution). */ -export interface AvailableVersionsExecuteArgs { +export interface AvailableVersionsExecuteArgs extends BaseExecuteArgs { packageName: string; pythonVersion: string; includePrerelease?: boolean; diff --git a/src/managers/base/commands/index.ts b/src/managers/base/commands/index.ts index fd225c50..41a16fd4 100644 --- a/src/managers/base/commands/index.ts +++ b/src/managers/base/commands/index.ts @@ -2,6 +2,6 @@ export { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from './a export { InstallCommand, type InstallExecuteArgs } from './install'; export { ListCommand } from './list'; export { ListDirectNamesCommand } from './listDirectNames'; -export { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +export { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; export { UninstallCommand, type UninstallExecuteArgs } from './uninstall'; export { VersionCommand } from './version'; diff --git a/src/managers/base/commands/install.ts b/src/managers/base/commands/install.ts index df9574dd..3f635b10 100644 --- a/src/managers/base/commands/install.ts +++ b/src/managers/base/commands/install.ts @@ -1,9 +1,9 @@ -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for install command execution (change per execution). */ -export interface InstallExecuteArgs { +export interface InstallExecuteArgs extends BaseExecuteArgs { packages: { packageName: string; version?: string }[]; upgrade?: boolean; } diff --git a/src/managers/base/commands/list.ts b/src/managers/base/commands/list.ts index 2f300c3e..0a8077cc 100644 --- a/src/managers/base/commands/list.ts +++ b/src/managers/base/commands/list.ts @@ -1,5 +1,5 @@ import { PackageInfo } from '../../../api'; -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for list commands. @@ -10,5 +10,5 @@ export abstract class ListCommand extends PackageManagerCommand { super({ ...options, configSection: 'listCommandArgs' }); } - abstract execute(): Promise; + abstract execute(executeArgs?: BaseExecuteArgs): Promise; } diff --git a/src/managers/base/commands/listDirectNames.ts b/src/managers/base/commands/listDirectNames.ts index 071173fa..793b92b9 100644 --- a/src/managers/base/commands/listDirectNames.ts +++ b/src/managers/base/commands/listDirectNames.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for listDirectNames commands. @@ -9,5 +9,5 @@ export abstract class ListDirectNamesCommand extends PackageManagerCommand { super({ ...options, configSection: 'listDirectNamesCommandArgs' }); } - abstract execute(): Promise; + abstract execute(executeArgs?: BaseExecuteArgs): Promise; } diff --git a/src/managers/base/commands/packageManagerCommand.ts b/src/managers/base/commands/packageManagerCommand.ts index 00977288..5028072b 100644 --- a/src/managers/base/commands/packageManagerCommand.ts +++ b/src/managers/base/commands/packageManagerCommand.ts @@ -1,6 +1,14 @@ import { CancellationToken, LogOutputChannel, WorkspaceConfiguration } from 'vscode'; import { getConfiguration } from '../../../common/workspace.apis'; +/** + * Base interface for all command execute arguments. + * Provides optional cancellation token that all commands can use. + */ +export interface BaseExecuteArgs { + cancellationToken?: CancellationToken; +} + /** * Constructor options shared by all package manager commands. */ @@ -8,7 +16,6 @@ export interface CommandConstructorOptions { pythonExecutable: string; configSection?: string; log?: LogOutputChannel; - cancellationToken?: CancellationToken; } /** @@ -18,14 +25,12 @@ export interface CommandConstructorOptions { export abstract class PackageManagerCommand { protected pythonExecutable: string; protected log?: LogOutputChannel; - protected cancellationToken?: CancellationToken; protected timeout: number = 300000; protected config: WorkspaceConfiguration; constructor(options: CommandConstructorOptions) { this.pythonExecutable = options.pythonExecutable; this.log = options.log; - this.cancellationToken = options.cancellationToken; this.config = options.configSection ? getConfiguration(`python-envs.packageManager.${options.configSection}`) : getConfiguration('python-envs.packageManager'); @@ -34,5 +39,5 @@ export abstract class PackageManagerCommand { /** * Subclasses implement to build the command arguments. */ - protected abstract buildCommand(executeArgs: unknown): string[]; + protected abstract buildCommand(executeArgs: BaseExecuteArgs): string[]; } diff --git a/src/managers/base/commands/uninstall.ts b/src/managers/base/commands/uninstall.ts index 413d672a..31633868 100644 --- a/src/managers/base/commands/uninstall.ts +++ b/src/managers/base/commands/uninstall.ts @@ -1,9 +1,9 @@ -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Arguments for uninstall command execution (change per execution). */ -export interface UninstallExecuteArgs { +export interface UninstallExecuteArgs extends BaseExecuteArgs { packages: { packageName: string; version?: string }[]; } diff --git a/src/managers/base/commands/version.ts b/src/managers/base/commands/version.ts index 9fee8a20..1ffc6d3b 100644 --- a/src/managers/base/commands/version.ts +++ b/src/managers/base/commands/version.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; +import { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand'; /** * Template class for version commands. @@ -9,5 +9,5 @@ export abstract class VersionCommand extends PackageManagerCommand { super({ ...options, configSection: 'versionCommandArgs' }); } - abstract execute(): Promise; + abstract execute(executeArgs?: BaseExecuteArgs): Promise; } diff --git a/src/managers/builtin/commands/availableVersions.ts b/src/managers/builtin/commands/availableVersions.ts index 8d921c59..f03faf13 100644 --- a/src/managers/builtin/commands/availableVersions.ts +++ b/src/managers/builtin/commands/availableVersions.ts @@ -52,7 +52,7 @@ export class PipAvailableVersionsCommand extends AvailableVersionsCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs.cancellationToken, this.timeout, ); @@ -109,7 +109,7 @@ export class UvAvailableVersionsCommand extends AvailableVersionsCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs.cancellationToken, this.timeout, ); diff --git a/src/managers/builtin/commands/install.ts b/src/managers/builtin/commands/install.ts index d69f7a43..f67bda6e 100644 --- a/src/managers/builtin/commands/install.ts +++ b/src/managers/builtin/commands/install.ts @@ -41,7 +41,7 @@ export class PipInstallCommand extends InstallCommand { async execute(executeArgs: InstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); + await runPython(this.pythonExecutable, args, undefined, this.log, executeArgs.cancellationToken, this.timeout); } } @@ -84,6 +84,6 @@ export class UvInstallCommand extends InstallCommand { async execute(executeArgs: InstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); + await runPython(this.pythonExecutable, args, undefined, this.log, executeArgs.cancellationToken, this.timeout); } } diff --git a/src/managers/builtin/commands/list.ts b/src/managers/builtin/commands/list.ts index 738ce0c4..70239a16 100644 --- a/src/managers/builtin/commands/list.ts +++ b/src/managers/builtin/commands/list.ts @@ -1,5 +1,5 @@ import { PackageInfo } from '../../../api'; -import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, ListCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; /** @@ -19,7 +19,7 @@ export class PipListCommand extends ListCommand { return ['-m', 'pip', 'list', '--format=json']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { const packages: PackageInfo[] = []; const parser = (output: string): void => { @@ -45,7 +45,7 @@ export class PipListCommand extends ListCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); @@ -72,7 +72,7 @@ export class UvListCommand extends ListCommand { return ['pip', 'list', '--format=json']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { const packages: PackageInfo[] = []; const parser = (output: string): void => { @@ -98,7 +98,7 @@ export class UvListCommand extends ListCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); diff --git a/src/managers/builtin/commands/listDirectNames.ts b/src/managers/builtin/commands/listDirectNames.ts index d38b45bb..0acb195c 100644 --- a/src/managers/builtin/commands/listDirectNames.ts +++ b/src/managers/builtin/commands/listDirectNames.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, ListDirectNamesCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; /** @@ -19,7 +19,7 @@ export class PipListDirectNamesCommand extends ListDirectNamesCommand { return ['-m', 'pip', 'list', '--format=json', '--not-required']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { let directNames: string[] = []; const parser = (output: string): void => { @@ -37,7 +37,7 @@ export class PipListDirectNamesCommand extends ListDirectNamesCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); @@ -65,7 +65,7 @@ export class UvListDirectNamesCommand extends ListDirectNamesCommand { return ['pip', 'list', '--format=json', '--not-required']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { let directNames: string[] = []; const parser = (output: string): void => { @@ -83,7 +83,7 @@ export class UvListDirectNamesCommand extends ListDirectNamesCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); diff --git a/src/managers/builtin/commands/uninstall.ts b/src/managers/builtin/commands/uninstall.ts index b914c736..e3e5af72 100644 --- a/src/managers/builtin/commands/uninstall.ts +++ b/src/managers/builtin/commands/uninstall.ts @@ -21,7 +21,7 @@ export class PipUninstallCommand extends UninstallCommand { async execute(executeArgs: UninstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); + await runPython(this.pythonExecutable, args, undefined, this.log, executeArgs.cancellationToken, this.timeout); } } @@ -49,6 +49,6 @@ export class UvUninstallCommand extends UninstallCommand { async execute(executeArgs: UninstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPython(this.pythonExecutable, args, undefined, this.log, this.cancellationToken, this.timeout); + await runPython(this.pythonExecutable, args, undefined, this.log, executeArgs.cancellationToken, this.timeout); } } diff --git a/src/managers/builtin/commands/version.ts b/src/managers/builtin/commands/version.ts index 07342297..175f5f97 100644 --- a/src/managers/builtin/commands/version.ts +++ b/src/managers/builtin/commands/version.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, VersionCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runPython } from '../helpers'; /** @@ -18,7 +18,7 @@ export class PipVersionCommand extends VersionCommand { return ['-m', 'pip', '--version']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { let versionString: string = ''; const parser = (output: string): void => { @@ -34,7 +34,7 @@ export class PipVersionCommand extends VersionCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); @@ -61,7 +61,7 @@ export class UvVersionCommand extends VersionCommand { return ['--version']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { let versionString: string = ''; const parser = (output: string): void => { @@ -77,7 +77,7 @@ export class UvVersionCommand extends VersionCommand { args, undefined, this.log, - this.cancellationToken, + executeArgs?.cancellationToken, this.timeout, ); diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index 5a4f1670..10f01656 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -100,7 +100,6 @@ export class PipPackageManager implements PackageManager, Disposable { const manageCommandOptions: CommandConstructorOptions = { pythonExecutable, log: this.log, - cancellationToken: token, }; // Execute uninstall if needed @@ -108,7 +107,7 @@ export class PipPackageManager implements PackageManager, Disposable { const UninstallCommand = useUv ? UvUninstallCommand : PipUninstallCommand; const uninstallCmd = new UninstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute({ packages }); + await uninstallCmd.execute({ packages, cancellationToken: token }); } // Execute install if needed @@ -116,7 +115,7 @@ export class PipPackageManager implements PackageManager, Disposable { const InstallCommand = useUv ? UvInstallCommand : PipInstallCommand; const installCmd = new InstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toInstall); - await installCmd.execute({ packages, upgrade: options.upgrade }); + await installCmd.execute({ packages, upgrade: options.upgrade, cancellationToken: token }); } await updatePackagesAndNotify( @@ -175,7 +174,6 @@ export class PipPackageManager implements PackageManager, Disposable { const listCmd = new ListCmd({ pythonExecutable, log: this.log, - cancellationToken: undefined, }); const data = await listCmd.execute(); const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); @@ -197,7 +195,6 @@ export class PipPackageManager implements PackageManager, Disposable { const versionCmd = new VersionCmd({ pythonExecutable, log: this.log, - cancellationToken: undefined, }); const versionString = await versionCmd.execute(); return versionString ? (parse(versionString) ?? undefined) : undefined; @@ -226,7 +223,6 @@ export class PipPackageManager implements PackageManager, Disposable { const availableVersionsCmd = new AvailableVersionsCmd({ pythonExecutable, log: this.log, - cancellationToken: undefined, }); // For pip < 21.2.0, check version first @@ -270,7 +266,6 @@ export class PipPackageManager implements PackageManager, Disposable { const listDirectNamesCmd = new ListDirectNamesCmd({ pythonExecutable, log: this.log, - cancellationToken: undefined, }); const data = await listDirectNamesCmd.execute(); return data ? new Set(data.map(normalizePackageName)) : undefined; diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 8f60b081..628daf96 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -255,11 +255,7 @@ export async function getWorkspacePackagesToInstall( if (pythonExecutable) { const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); const ListCmd = useUv ? UvListCommand : PipListCommand; - const listCmd = new ListCmd({ - pythonExecutable, - log, - cancellationToken: undefined, - }); + const listCmd = new ListCmd({ pythonExecutable, log }); const data = await withProgress( { location: ProgressLocation.Notification, diff --git a/src/managers/conda/commands/availableVersions.ts b/src/managers/conda/commands/availableVersions.ts index 467d304a..0181c2db 100644 --- a/src/managers/conda/commands/availableVersions.ts +++ b/src/managers/conda/commands/availableVersions.ts @@ -27,7 +27,7 @@ export class CondaAvailableVersionsCommand extends AvailableVersionsCommand { async execute(executeArgs: AvailableVersionsExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - const output = await runCondaExecutable(args, this.log, this.cancellationToken); + const output = await runCondaExecutable(args, this.log, executeArgs.cancellationToken); try { const parsed = JSON.parse(output); diff --git a/src/managers/conda/commands/install.ts b/src/managers/conda/commands/install.ts index 34dddf0b..ccf14453 100644 --- a/src/managers/conda/commands/install.ts +++ b/src/managers/conda/commands/install.ts @@ -39,6 +39,6 @@ export class CondaInstallCommand extends InstallCommand { async execute(executeArgs: InstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runCondaExecutable(args, this.log, this.cancellationToken); + await runCondaExecutable(args, this.log, executeArgs.cancellationToken); } } diff --git a/src/managers/conda/commands/list.ts b/src/managers/conda/commands/list.ts index 4ab3b464..60452581 100644 --- a/src/managers/conda/commands/list.ts +++ b/src/managers/conda/commands/list.ts @@ -1,11 +1,11 @@ import { PackageInfo } from '../../../api'; -import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, ListCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; /** - * Ephemeral arguments for conda list command (change per execution). + * Conda list command execute arguments (includes environment path and cancellation token). */ -interface ListEphemeralArgs { +interface CondaListExecuteArgs extends BaseExecuteArgs { environmentPath: string; } @@ -24,17 +24,19 @@ export class CondaListCommand extends ListCommand { super(options); } - protected buildCommand(ephemeralArgs: ListEphemeralArgs): string[] { - return ['list', '-p', ephemeralArgs.environmentPath, '--json']; + protected buildCommand(executeArgs: BaseExecuteArgs): string[] { + const args = executeArgs as CondaListExecuteArgs; + return ['list', '-p', args.environmentPath, '--json']; } - async execute(environmentPath?: string): Promise { - if (!environmentPath) { + async execute(executeArgs?: BaseExecuteArgs): Promise { + const args = executeArgs as CondaListExecuteArgs | undefined; + if (!args?.environmentPath) { return []; } - const args = this.buildCommand({ environmentPath }); - const output = await runCondaExecutable(args, this.log, this.cancellationToken); + const cmdArgs = this.buildCommand(executeArgs!); + const output = await runCondaExecutable(cmdArgs, this.log, executeArgs?.cancellationToken); let condaPackages: { name: string; version: string }[]; try { diff --git a/src/managers/conda/commands/uninstall.ts b/src/managers/conda/commands/uninstall.ts index ce9ed93c..7911a744 100644 --- a/src/managers/conda/commands/uninstall.ts +++ b/src/managers/conda/commands/uninstall.ts @@ -23,6 +23,6 @@ export class CondaUninstallCommand extends UninstallCommand { async execute(executeArgs: UninstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runCondaExecutable(args, this.log, this.cancellationToken); + await runCondaExecutable(args, this.log, executeArgs.cancellationToken); } } diff --git a/src/managers/conda/commands/version.ts b/src/managers/conda/commands/version.ts index 57663a26..42075068 100644 --- a/src/managers/conda/commands/version.ts +++ b/src/managers/conda/commands/version.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, VersionCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, VersionCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runCondaExecutable } from '../condaUtils'; /** @@ -19,9 +19,9 @@ export class CondaVersionCommand extends VersionCommand { return ['--version']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { const args = this.buildCommand(); - const output = await runCondaExecutable(args, this.log, this.cancellationToken); + const output = await runCondaExecutable(args, this.log, executeArgs?.cancellationToken); // "conda X.Y.Z" const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 1d06865c..7f8bec5d 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -83,21 +83,20 @@ export class CondaPackageManager implements PackageManager, Disposable { const manageCommandOptions: CommandConstructorOptions = { pythonExecutable: 'conda', log: this.log, - cancellationToken: token, }; // Execute uninstall if needed if (toUninstall.length > 0) { const uninstallCmd = new CondaUninstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute({ packages }); + await uninstallCmd.execute({ packages, cancellationToken: token }); } // Execute install if needed if (toInstall.length > 0) { const installCmd = new CondaInstallCommand(manageCommandOptions); const packages = parsePackageSpecs(toInstall); - await installCmd.execute({ packages, upgrade: options.upgrade }); + await installCmd.execute({ packages, upgrade: options.upgrade, cancellationToken: token }); } await updatePackagesAndNotify( @@ -146,9 +145,8 @@ export class CondaPackageManager implements PackageManager, Disposable { const listCmd = new CondaListCommand({ pythonExecutable: 'conda', log: this.log, - cancellationToken: undefined, }); - const data = await listCmd.execute(environment.environmentPath.fsPath); + const data = await listCmd.execute({ environmentPath: environment.environmentPath.fsPath } as any); const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); this.packages.set(environment.envId.id, packages); return packages; @@ -166,7 +164,6 @@ export class CondaPackageManager implements PackageManager, Disposable { const versionCmd = new CondaVersionCommand({ pythonExecutable: 'conda', log: this.log, - cancellationToken: undefined, }); const versionString = await versionCmd.execute(); return versionString ? (parse(versionString) ?? undefined) : undefined; @@ -183,7 +180,6 @@ export class CondaPackageManager implements PackageManager, Disposable { const availableVersionsCmd = new CondaAvailableVersionsCommand({ pythonExecutable: 'conda', log: this.log, - cancellationToken: undefined, }); const versionStrings = await availableVersionsCmd.execute({ packageName, pythonVersion: '' }); return versionStrings.map((v) => parse(v)).filter((parsed) => parsed !== undefined) as Pep440Version[]; diff --git a/src/managers/poetry/commands/add.ts b/src/managers/poetry/commands/add.ts index fddf52ef..501b710d 100644 --- a/src/managers/poetry/commands/add.ts +++ b/src/managers/poetry/commands/add.ts @@ -36,6 +36,6 @@ export class PoetryAddCommand extends InstallCommand { async execute(executeArgs: InstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPoetry(args, undefined, this.log, this.cancellationToken); + await runPoetry(args, undefined, this.log, executeArgs.cancellationToken); } } diff --git a/src/managers/poetry/commands/remove.ts b/src/managers/poetry/commands/remove.ts index 806901f2..da31c590 100644 --- a/src/managers/poetry/commands/remove.ts +++ b/src/managers/poetry/commands/remove.ts @@ -21,6 +21,6 @@ export class PoetryRemoveCommand extends UninstallCommand { async execute(executeArgs: UninstallExecuteArgs): Promise { const args = this.buildCommand(executeArgs); - await runPoetry(args, undefined, this.log, this.cancellationToken); + await runPoetry(args, undefined, this.log, executeArgs.cancellationToken); } } diff --git a/src/managers/poetry/commands/show.ts b/src/managers/poetry/commands/show.ts index 227e1fb9..3832b40e 100644 --- a/src/managers/poetry/commands/show.ts +++ b/src/managers/poetry/commands/show.ts @@ -1,5 +1,5 @@ import { PackageInfo } from '../../../api'; -import { CommandConstructorOptions, ListCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, ListCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; /** @@ -20,9 +20,9 @@ export class PoetryShowCommand extends ListCommand { return ['show', '--no-ansi']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { const args = this.buildCommand(); - const output = await runPoetry(args, undefined, this.log, this.cancellationToken); + const output = await runPoetry(args, undefined, this.log, executeArgs?.cancellationToken); const packages: PackageInfo[] = []; diff --git a/src/managers/poetry/commands/showTopLevel.ts b/src/managers/poetry/commands/showTopLevel.ts index 14285716..ff6694de 100644 --- a/src/managers/poetry/commands/showTopLevel.ts +++ b/src/managers/poetry/commands/showTopLevel.ts @@ -1,4 +1,4 @@ -import { CommandConstructorOptions, ListDirectNamesCommand } from '../../base/commands/index'; +import { CommandConstructorOptions, ListDirectNamesCommand, type BaseExecuteArgs } from '../../base/commands/index'; import { runPoetry } from '../poetryPackageManager'; /** @@ -20,9 +20,9 @@ export class PoetryShowTopLevelCommand extends ListDirectNamesCommand { return ['show', '--no-ansi', '--top-level']; } - async execute(): Promise { + async execute(executeArgs?: BaseExecuteArgs): Promise { const args = this.buildCommand(); - const output = await runPoetry(args, undefined, this.log, this.cancellationToken); + const output = await runPoetry(args, undefined, this.log, executeArgs?.cancellationToken); try { const names = output diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index e36eaf4a..b0845f14 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -13,7 +13,6 @@ import { } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { - CommandConstructorOptions, DidChangePackagesEventArgs, GetPackagesOptions, IconPath, @@ -165,7 +164,6 @@ export class PoetryPackageManager implements PackageManager, Disposable { const versionCmd = new PoetryVersionCommand({ pythonExecutable: poetry, log: this.log, - cancellationToken: undefined, }); const versionString = await versionCmd.execute(); return versionString ? (parse(versionString) ?? undefined) : undefined; @@ -204,25 +202,24 @@ export class PoetryPackageManager implements PackageManager, Disposable { ); } - // Centralize command options for install/uninstall operations - const manageCommandOptions: CommandConstructorOptions = { - pythonExecutable: poetry, - log: this.log, - cancellationToken: token, - }; - // Handle uninstalls first if (options.uninstall && options.uninstall.length > 0) { - const removeCmd = new PoetryRemoveCommand(manageCommandOptions); + const removeCmd = new PoetryRemoveCommand({ + pythonExecutable: poetry, + log: this.log, + }); const packages = parsePackageSpecs(options.uninstall); - await removeCmd.execute({ packages }); + await removeCmd.execute({ packages, cancellationToken: token }); } // Handle installs if (options.install && options.install.length > 0) { - const addCmd = new PoetryAddCommand(manageCommandOptions); + const addCmd = new PoetryAddCommand({ + pythonExecutable: poetry, + log: this.log, + }); const packages = parsePackageSpecs(options.install); - await addCmd.execute({ packages }); + await addCmd.execute({ packages, cancellationToken: token }); } } @@ -239,7 +236,6 @@ export class PoetryPackageManager implements PackageManager, Disposable { const showCmd = new PoetryShowCommand({ pythonExecutable: poetry, log: this.log, - cancellationToken: undefined, }); const data = await showCmd.execute(); return (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); @@ -254,7 +250,6 @@ export class PoetryPackageManager implements PackageManager, Disposable { const showTopLevelCmd = new PoetryShowTopLevelCommand({ pythonExecutable: poetry, log: this.log, - cancellationToken: undefined, }); const names = await showTopLevelCmd.execute(); return names ? new Set(names.map(normalizePackageName)) : undefined; From b41a54d65596255a0f36f0a804f92ec49ab834f9 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 12:17:31 +0200 Subject: [PATCH 25/28] runPython: Do not return error --- src/managers/builtin/helpers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/managers/builtin/helpers.ts b/src/managers/builtin/helpers.ts index 911bc603..c5f238c8 100644 --- a/src/managers/builtin/helpers.ts +++ b/src/managers/builtin/helpers.ts @@ -131,9 +131,7 @@ export async function runPython( log?.append(`python: ${s}`); }); proc.stderr?.on('data', (data) => { - const s = data.toString('utf-8'); - builder += s; - log?.append(`python: ${s}`); + log?.append(data.toString('utf-8')); }); proc.on('close', () => { resolve(builder); From 64c2428f2634d437c5159d1f73b4aa8b4950587f Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 12:21:27 +0200 Subject: [PATCH 26/28] Remove unnecessary params --- src/managers/conda/condaPackageManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 7f8bec5d..9c38eaca 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -146,7 +146,7 @@ export class CondaPackageManager implements PackageManager, Disposable { pythonExecutable: 'conda', log: this.log, }); - const data = await listCmd.execute({ environmentPath: environment.environmentPath.fsPath } as any); + const data = await listCmd.execute(); const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this)); this.packages.set(environment.envId.id, packages); return packages; From dd3efc69392bc8627a3b325013c4ef9cbfa84011 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 13:06:03 +0200 Subject: [PATCH 27/28] Extract showProgress --- .../base/commands/packageManagerCommand.ts | 30 +++++- src/managers/builtin/pipPackageManager.ts | 97 ++++++++----------- src/managers/builtin/pipUtils.ts | 7 +- src/managers/conda/condaPackageManager.ts | 85 +++++++--------- src/managers/poetry/poetryPackageManager.ts | 54 ++++------- 5 files changed, 124 insertions(+), 149 deletions(-) diff --git a/src/managers/base/commands/packageManagerCommand.ts b/src/managers/base/commands/packageManagerCommand.ts index 5028072b..870d1918 100644 --- a/src/managers/base/commands/packageManagerCommand.ts +++ b/src/managers/base/commands/packageManagerCommand.ts @@ -1,4 +1,4 @@ -import { CancellationToken, LogOutputChannel, WorkspaceConfiguration } from 'vscode'; +import { CancellationToken, l10n, LogOutputChannel, ProgressLocation, window, WorkspaceConfiguration } from 'vscode'; import { getConfiguration } from '../../../common/workspace.apis'; /** @@ -7,6 +7,7 @@ import { getConfiguration } from '../../../common/workspace.apis'; */ export interface BaseExecuteArgs { cancellationToken?: CancellationToken; + showProgress?: boolean; } /** @@ -36,6 +37,33 @@ export abstract class PackageManagerCommand { : getConfiguration('python-envs.packageManager'); } + /** + * Executes this command and optionally wraps execution with a progress indicator. + */ + public executeWithProgress( + executeArgs?: A, + title?: string, + ): Promise { + if (!executeArgs?.showProgress) { + return this.execute(executeArgs) as Promise; + } + + return Promise.resolve( + window.withProgress( + { + location: ProgressLocation.Notification, + title: title ?? l10n.t('Running package manager command'), + }, + () => this.execute(executeArgs) as Promise, + ), + ); + } + + /** + * Subclasses implement command execution. + */ + abstract execute(executeArgs?: BaseExecuteArgs): Promise; + /** * Subclasses implement to build the command arguments. */ diff --git a/src/managers/builtin/pipPackageManager.ts b/src/managers/builtin/pipPackageManager.ts index 10f01656..249edfb2 100644 --- a/src/managers/builtin/pipPackageManager.ts +++ b/src/managers/builtin/pipPackageManager.ts @@ -1,7 +1,6 @@ import type { Pep440Version } from '@renovatebot/pep440'; import { compare, explain as parse } from '@renovatebot/pep440'; import { - CancellationError, Disposable, Event, EventEmitter, @@ -80,67 +79,53 @@ export class PipPackageManager implements PackageManager, Disposable { } } - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Installing packages', - cancellable: true, - }, - async (_progress, token) => { - try { - const pythonExecutable = environment.execInfo?.run?.executable; - if (!pythonExecutable) { - throw new Error('Unable to determine Python executable path'); - } + try { + const pythonExecutable = environment.execInfo?.run?.executable; + if (!pythonExecutable) { + throw new Error('Unable to determine Python executable path'); + } - // Detect whether to use UV - const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + // Detect whether to use UV + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); - // Centralize command options for install/uninstall operations - const manageCommandOptions: CommandConstructorOptions = { - pythonExecutable, - log: this.log, - }; + // Centralize command options for install/uninstall operations + const manageCommandOptions: CommandConstructorOptions = { + pythonExecutable, + log: this.log, + }; - // Execute uninstall if needed - if (toUninstall.length > 0) { - const UninstallCommand = useUv ? UvUninstallCommand : PipUninstallCommand; - const uninstallCmd = new UninstallCommand(manageCommandOptions); - const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute({ packages, cancellationToken: token }); - } + // Execute uninstall if needed + if (toUninstall.length > 0) { + const UninstallCommand = useUv ? UvUninstallCommand : PipUninstallCommand; + const uninstallCmd = new UninstallCommand(manageCommandOptions); + const packages = parsePackageSpecs(toUninstall); + await uninstallCmd.executeWithProgress({ packages, showProgress: true }, 'Installing packages'); + } - // Execute install if needed - if (toInstall.length > 0) { - const InstallCommand = useUv ? UvInstallCommand : PipInstallCommand; - const installCmd = new InstallCommand(manageCommandOptions); - const packages = parsePackageSpecs(toInstall); - await installCmd.execute({ packages, upgrade: options.upgrade, cancellationToken: token }); - } + // Execute install if needed + if (toInstall.length > 0) { + const InstallCommand = useUv ? UvInstallCommand : PipInstallCommand; + const installCmd = new InstallCommand(manageCommandOptions); + const packages = parsePackageSpecs(toInstall); + await installCmd.executeWithProgress( + { packages, upgrade: options.upgrade, showProgress: true }, + 'Installing packages', + ); + } - await updatePackagesAndNotify( - this, - environment, - this.packages.get(environment.envId.id), - (changes) => { - this._onDidChangePackages.fire({ environment, manager: this, changes }); - }, - ); - } catch (e) { - if (e instanceof CancellationError) { - throw e; - } - this.log.error('Error managing packages', e); - setImmediate(async () => { - const result = await window.showErrorMessage('Error managing packages', 'View Output'); - if (result === 'View Output') { - this.log.show(); - } - }); - throw e; + await updatePackagesAndNotify(this, environment, this.packages.get(environment.envId.id), (changes) => { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + }); + } catch (e) { + this.log.error('Error managing packages', e); + setImmediate(async () => { + const result = await window.showErrorMessage('Error managing packages', 'View Output'); + if (result === 'View Output') { + this.log.show(); } - }, - ); + }); + throw e; + } } async refresh(environment: PythonEnvironment): Promise { diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 628daf96..b561e35e 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -256,12 +256,7 @@ export async function getWorkspacePackagesToInstall( const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); const ListCmd = useUv ? UvListCommand : PipListCommand; const listCmd = new ListCmd({ pythonExecutable, log }); - const data = await withProgress( - { - location: ProgressLocation.Notification, - }, - async () => await listCmd.execute(), - ); + const data = await listCmd.executeWithProgress<{ name: string }[]>({ showProgress: true }); installed = data?.map((pkg) => pkg.name); } common = mergePackages(common, installed ?? []); diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 9c38eaca..27065873 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -2,7 +2,6 @@ import type { Pep440Version } from '@renovatebot/pep440'; import { explain as parse } from '@renovatebot/pep440'; import * as path from 'path'; import { - CancellationError, Disposable, Event, EventEmitter, @@ -71,54 +70,42 @@ export class CondaPackageManager implements PackageManager, Disposable { } } - await withProgress( - { - location: ProgressLocation.Notification, - title: CondaStrings.condaInstallingPackages, - cancellable: true, - }, - async (_progress, token) => { - try { - // Centralize command options for install/uninstall operations - const manageCommandOptions: CommandConstructorOptions = { - pythonExecutable: 'conda', - log: this.log, - }; - - // Execute uninstall if needed - if (toUninstall.length > 0) { - const uninstallCmd = new CondaUninstallCommand(manageCommandOptions); - const packages = parsePackageSpecs(toUninstall); - await uninstallCmd.execute({ packages, cancellationToken: token }); - } - - // Execute install if needed - if (toInstall.length > 0) { - const installCmd = new CondaInstallCommand(manageCommandOptions); - const packages = parsePackageSpecs(toInstall); - await installCmd.execute({ packages, upgrade: options.upgrade, cancellationToken: token }); - } - - await updatePackagesAndNotify( - this, - environment, - this.packages.get(environment.envId.id), - (changes) => { - this._onDidChangePackages.fire({ environment, manager: this, changes }); - }, - ); - } catch (e) { - if (e instanceof CancellationError) { - throw e; - } - - this.log.error('Error installing packages', e); - setImmediate(async () => { - await showErrorMessageWithLogs(CondaStrings.condaInstallError, this.log); - }); - } - }, - ); + try { + // Centralize command options for install/uninstall operations + const manageCommandOptions: CommandConstructorOptions = { + pythonExecutable: 'conda', + log: this.log, + }; + + // Execute uninstall if needed + if (toUninstall.length > 0) { + const uninstallCmd = new CondaUninstallCommand(manageCommandOptions); + const packages = parsePackageSpecs(toUninstall); + await uninstallCmd.executeWithProgress( + { packages, showProgress: true }, + CondaStrings.condaInstallingPackages, + ); + } + + // Execute install if needed + if (toInstall.length > 0) { + const installCmd = new CondaInstallCommand(manageCommandOptions); + const packages = parsePackageSpecs(toInstall); + await installCmd.executeWithProgress( + { packages, upgrade: options.upgrade, showProgress: true }, + CondaStrings.condaInstallingPackages, + ); + } + + await updatePackagesAndNotify(this, environment, this.packages.get(environment.envId.id), (changes) => { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + }); + } catch (e) { + this.log.error('Error installing packages', e); + setImmediate(async () => { + await showErrorMessageWithLogs(CondaStrings.condaInstallError, this.log); + }); + } } async refresh(environment: PythonEnvironment): Promise { diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index b0845f14..637ba0df 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -82,38 +82,21 @@ export class PoetryPackageManager implements PackageManager, Disposable { } } - await withProgress( - { - location: ProgressLocation.Notification, - title: 'Managing packages with Poetry', - cancellable: true, - }, - async (_progress, token) => { - try { - await this.runPoetryManage({ install: toInstall, uninstall: toUninstall }, token); - await updatePackagesAndNotify( - this, - environment, - this.packages.get(environment.envId.id), - (changes) => { - this._onDidChangePackages.fire({ environment, manager: this, changes }); - }, - ); - } catch (e) { - if (e instanceof CancellationError) { - throw e; - } - this.log.error('Error managing packages with Poetry', e); - setImmediate(async () => { - const result = await showErrorMessage('Error managing packages with Poetry', 'View Output'); - if (result === 'View Output') { - this.log.show(); - } - }); - throw e; + try { + await this.runPoetryManage({ install: toInstall, uninstall: toUninstall }); + await updatePackagesAndNotify(this, environment, this.packages.get(environment.envId.id), (changes) => { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + }); + } catch (e) { + this.log.error('Error managing packages with Poetry', e); + setImmediate(async () => { + const result = await showErrorMessage('Error managing packages with Poetry', 'View Output'); + if (result === 'View Output') { + this.log.show(); } - }, - ); + }); + throw e; + } } async refresh(environment: PythonEnvironment): Promise { @@ -189,10 +172,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { this.packages.clear(); } - private async runPoetryManage( - options: { install?: string[]; uninstall?: string[] }, - token?: CancellationToken, - ): Promise { + private async runPoetryManage(options: { install?: string[]; uninstall?: string[] }): Promise { const poetry = await getPoetry(); if (!poetry) { throw new Error( @@ -209,7 +189,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { log: this.log, }); const packages = parsePackageSpecs(options.uninstall); - await removeCmd.execute({ packages, cancellationToken: token }); + await removeCmd.executeWithProgress({ packages, showProgress: true }, 'Managing packages with Poetry'); } // Handle installs @@ -219,7 +199,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { log: this.log, }); const packages = parsePackageSpecs(options.install); - await addCmd.execute({ packages, cancellationToken: token }); + await addCmd.executeWithProgress({ packages, showProgress: true }, 'Managing packages with Poetry'); } } From 804ee695ed57317ff353d229413b7dfdae530431 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sun, 5 Jul 2026 13:15:18 +0200 Subject: [PATCH 28/28] Remove unused method --- src/managers/builtin/pipUtils.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index b561e35e..254872bf 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -398,17 +398,3 @@ export async function shouldProceedAfterPyprojectValidation( return false; } - -export function isPipInstallCommand(command: string): boolean { - // Regex to match pip install commands, capturing variations like: - // pip install package - // python -m pip install package - // pip3 install package - // py -m pip install package - // pip install -r requirements.txt - // uv pip install package - // poetry run pip install package - // pipx run pip install package - // Any other tool that might wrap pip install - return /(?:^|\s)(?:\S+\s+)*(?:pip\d*)\s+(install|uninstall)\b/.test(command); -}