Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions openspec/changes/add-pip-package-manager/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-12
66 changes: 66 additions & 0 deletions openspec/changes/add-pip-package-manager/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Design

pip should fit the existing managed installer abstraction instead of introducing a separate Python-specific lifecycle path. A pip install method is managed because Quantex can invoke deterministic install, update, and uninstall commands for a named Python package.

## Decisions

### 1. Model pip as a managed install type

Add `pip` to `ManagedInstallType` and the installer capability table with:

- `canInstall: true`
- `canUpdate: true`
- `canUninstall: true`
- `canLookupLatestVersion: false`

pip package version lookup via PyPI API is intentionally out of scope for this change. The initial implementation focuses on reliable install/update/uninstall operations without adding PyPI registry queries.

### 2. Use pip's standard global commands

The pip installer executes:

- install: `pip install <package>`
- update: `pip install --upgrade <package>`
- uninstall: `pip uninstall -y <package>`

All commands use the `pip` binary detected in PATH. If `pip` is not available, the fallback detection tries `pip3` and then `python -m pip`.

### 3. Keep package metadata explicit

Add optional `packages.pip` metadata and a `pipInstall(packageName?: string)` helper. `getManagedPackageName` resolves in this order:

1. method-level `packageName`
2. installer-specific package metadata such as `packages.pip`
3. npm metadata only for npm-compatible installers (`npm` and `bun`)

This avoids accidentally using an npm package name as a pip package name.

### 4. Do not make pip a default package manager

The `defaultPackageManager` config remains scoped to `bun` and `npm`. pip is not a general substitute for npm-compatible install methods and should only be selected when an agent definition explicitly offers a pip method.

### 5. Cross-platform pip detection

pip availability detection follows this order:

1. Try `pip --version`
2. Try `pip3 --version`
3. Try `python -m pip --version`

Once a working pip command is found, it is used for all subsequent operations in that session. This approach handles common scenarios:

- Systems where only `pip3` is available (common on macOS/Linux)
- Systems where pip is only available as a Python module
- Systems with both `pip` and `pip3` available

## Non-goals

- Add Quantex self-upgrade through pip.
- Add PyPI latest-version lookup.
- Infer pip-managed installs from arbitrary binary paths.
- Manage Python virtual environments, pyenv, asdf, or uv tool installs.

## Risks

- pip may require elevated permissions on some systems for global installs. The current implementation does not add `--user` flag automatically; users should ensure their pip configuration supports global installs or use `--user` flag externally.
- Some packages may have system-level dependencies that pip cannot resolve. This is acceptable because Quantex focuses on the package manager lifecycle, not system dependency resolution.
39 changes: 39 additions & 0 deletions openspec/changes/add-pip-package-manager/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Why

Some lifecycle-managed coding agents are distributed as Python packages via pip (e.g., Mistral Vibe). Quantex currently has managed installers for Bun, npm, Homebrew, Cargo, and winget, but it cannot model or execute a first-class pip install path. Python-based agent CLIs are therefore represented as unmanaged binary command hints instead of managed lifecycle methods.

This work is OpenSpec-required because it changes agent catalog install-method metadata, managed lifecycle execution, diagnostic output, and batch update planning.

## What Changes

- Add `pip` as a managed package-manager install type.
- Add `pip` to `AgentPackageMetadata` for agent definitions.
- Detect whether `pip` is available in `PATH`, with fallback to `python -m pip` detection.
- Execute pip-managed install, update, batch update, and uninstall operations through pip commands.
- Render pip install guidance in resolve/exec/list/info surfaces that already expose install methods.
- Include pip availability in `capabilities` and `doctor` diagnostics.
- Migrate verified pip-based agent definitions (starting with Mistral Vibe) from unmanaged `binaryInstall(...)` to the new managed `pipInstall(...)` method.
- Keep Quantex self-upgrade and the `defaultPackageManager` configuration scoped to the existing supported self-install sources; pip is an agent lifecycle installer, not a Quantex self-upgrade provider.

## Capabilities

### New Capabilities

- None.

### Modified Capabilities

- `agent-catalog`: Supported agent entries may declare pip managed install methods and package metadata.
- `agent-update`: pip-managed installs participate in managed install, update, batch update, uninstall, and diagnostic planning.

## Impact

- Affected code: `src/agents/types.ts`, `src/agents/methods.ts`, `src/package-manager/`, `src/utils/detect.ts`, `src/commands/capabilities.ts`, `src/commands/doctor.ts`, agent definitions, and related tests.
- Affected structured output: `quantex capabilities --json` and `quantex doctor --json` installer maps gain a `pip` key.
- No new runtime dependency is required.

## Cross-Platform Considerations

- Windows: Use `pip` command directly or `python -m pip` as fallback.
- macOS/Linux: Use `pip` command directly or `python -m pip` as fallback. Some systems may require `pip3` instead of `pip`.
- The pip command detection will try `pip`, then `pip3`, then `python -m pip` as fallback strategies.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## ADDED Requirements

### Requirement: pip install methods MUST be supported lifecycle metadata

Quantex SHALL allow supported agent catalog entries to declare pip-managed install methods and pip package metadata when an upstream agent is distributed as a Python package.

#### Scenario: Registering pip package metadata

- **WHEN** Quantex defines or updates a supported agent entry that is distributed as a Python package
- **THEN** the entry can identify the package through `packages.pip`
- **AND** the entry can include pip managed install methods on platforms where the package is supported
- **AND** pip package metadata is treated as lifecycle metadata, not descriptive marketing copy

#### Scenario: Rendering pip install guidance

- **WHEN** Quantex renders install methods for an agent with a pip managed install method
- **THEN** the install method is labeled as a managed pip install
- **AND** the command guidance uses `pip install <package>`

#### Scenario: Registering Mistral Vibe pip metadata

- **WHEN** Quantex defines the supported Mistral Vibe agent entry
- **THEN** the entry identifies `mistral-vibe` as pip package metadata
- **AND** the pip install method is included as a managed install method on Windows, macOS, and Linux
- **AND** the entry continues to support other install methods such as script installers
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## ADDED Requirements

### Requirement: pip-managed agent lifecycle MUST use pip commands

pip-managed agent lifecycle operations SHALL install, update, batch update, uninstall, and diagnose agents through the pip installer when the recorded or selected install source is pip.

#### Scenario: Updating pip-managed agents

- **GIVEN** an agent has recorded install state with install type `pip`
- **WHEN** the user runs `quantex update <agent>` or `quantex update --all`
- **THEN** Quantex selects the pip managed update path
- **AND** it runs pip with the recorded package name and `--upgrade` flag instead of guessing another package-manager source

#### Scenario: Grouping pip-managed updates

- **GIVEN** multiple installed agents have recorded pip install state
- **WHEN** the user runs `quantex update --all`
- **THEN** Quantex groups those updates by the pip installer
- **AND** it executes pip-managed batch update work without mixing the packages into npm, Bun, Homebrew, Cargo, or winget groups

#### Scenario: Reporting pip installer availability

- **GIVEN** the user runs `quantex capabilities` or `quantex doctor`
- **WHEN** Quantex reports managed installer availability
- **THEN** the output includes pip availability alongside Bun, npm, Homebrew, Cargo, and winget
20 changes: 20 additions & 0 deletions openspec/changes/add-pip-package-manager/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. Core Implementation

- [x] 1.1 Add pip to managed install types, package metadata, install-method helpers, and installer capability classification.
- [x] 1.2 Add pip availability detection and pip installer implementation.
- [x] 1.3 Wire pip into managed installer lookup, install/update/uninstall execution, and batch update grouping.
- [x] 1.4 Render pip install commands and expose pip availability in `capabilities` and `doctor`.

## 2. Agent Migration

- [x] 2.1 Migrate Mistral Vibe agent definition from `binaryInstall('pip install mistral-vibe')` to managed `pipInstall('mistral-vibe')`.

## 3. Tests

- [x] 3.1 Add or update unit coverage for pip installer behavior, command rendering, update planning, and diagnostics.

## 4. Validation

- [x] 4.1 Run `bun run openspec:status -- --change add-pip-package-manager`.
- [x] 4.2 Run `bun run openspec:validate`.
- [x] 4.3 Run `bun run lint`, `bun run format:check`, `bun run typecheck`, and `bun run test`.
11 changes: 7 additions & 4 deletions src/agents/definitions/vibe.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import type { AgentDefinition } from '../types'
import { binaryInstall, scriptInstall } from '../methods'
import { binaryInstall, pipInstall, scriptInstall } from '../methods'

export const vibe: AgentDefinition = {
name: 'vibe',
lookupAliases: ['mistral-vibe'],
displayName: 'Mistral Vibe',
homepage: 'https://docs.mistral.ai/mistral-vibe/terminal/install',
binaryName: 'vibe',
packages: {
pip: 'mistral-vibe',
},
versionProbe: {
command: ['vibe', '--version'],
},
platforms: {
windows: [binaryInstall('uv tool install mistral-vibe'), binaryInstall('pip install mistral-vibe')],
windows: [binaryInstall('uv tool install mistral-vibe'), pipInstall('mistral-vibe')],
macos: [
scriptInstall('curl -LsSf https://mistral.ai/vibe/install.sh | bash'),
binaryInstall('uv tool install mistral-vibe'),
binaryInstall('pip install mistral-vibe'),
pipInstall('mistral-vibe'),
],
linux: [
scriptInstall('curl -LsSf https://mistral.ai/vibe/install.sh | bash'),
binaryInstall('uv tool install mistral-vibe'),
binaryInstall('pip install mistral-vibe'),
pipInstall('mistral-vibe'),
],
},
}
7 changes: 7 additions & 0 deletions src/agents/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export function cargoInstall(packageName?: string, packageInstallArgs?: string[]
}
}

export function pipInstall(packageName?: string): ManagedInstallMethod {
return {
packageName,
type: 'pip',
}
}

export function wingetInstall(packageName: string): ManagedInstallMethod {
return {
packageName,
Expand Down
3 changes: 2 additions & 1 deletion src/agents/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type Platform = 'windows' | 'macos' | 'linux'

export type ManagedInstallType = 'bun' | 'npm' | 'brew' | 'cargo' | 'winget'
export type ManagedInstallType = 'bun' | 'npm' | 'brew' | 'cargo' | 'pip' | 'winget'
export type InstallType = ManagedInstallType | 'script' | 'binary'
export type PackageTargetKind = 'package' | 'cask' | 'id'

Expand Down Expand Up @@ -31,6 +31,7 @@ export type InstallMethod = ManagedInstallMethod | ScriptInstallMethod | BinaryI
export interface AgentPackageMetadata {
cargo?: string
npm?: string
pip?: string
}

export interface AgentSelfUpdate {
Expand Down
21 changes: 16 additions & 5 deletions src/commands/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isBunAvailable,
isCargoAvailable,
isNpmAvailable,
isPipAvailable,
isWingetAvailable,
} from '../utils/detect'

Expand Down Expand Up @@ -47,6 +48,10 @@ interface CapabilitiesData {
available: boolean
reason?: string
}
pip: {
available: boolean
reason?: string
}
winget: {
available: boolean
reason?: string
Expand All @@ -60,12 +65,13 @@ interface CapabilitiesData {
}

export async function capabilitiesCommand(): Promise<CommandResult<CapabilitiesData>> {
const [bunAvailable, npmAvailable, brewAvailable, cargoAvailable, wingetAvailable, selfInspection] =
const [bunAvailable, npmAvailable, brewAvailable, cargoAvailable, pipAvailable, wingetAvailable, selfInspection] =
await Promise.all([
isBunAvailable(),
isNpmAvailable(),
isBrewAvailable(),
isCargoAvailable(),
isPipAvailable(),
isWingetAvailable(),
inspectSelf(),
])
Expand Down Expand Up @@ -99,13 +105,17 @@ export async function capabilitiesCommand(): Promise<CommandResult<CapabilitiesD
available: bunAvailable,
reason: bunAvailable ? undefined : getUnavailableReason('bun'),
},
cargo: {
available: cargoAvailable,
reason: cargoAvailable ? undefined : getUnavailableReason('cargo'),
},
npm: {
available: npmAvailable,
reason: npmAvailable ? undefined : getUnavailableReason('npm'),
},
cargo: {
available: cargoAvailable,
reason: cargoAvailable ? undefined : getUnavailableReason('cargo'),
pip: {
available: pipAvailable,
reason: pipAvailable ? undefined : getUnavailableReason('pip'),
},
winget: {
available: wingetAvailable,
Expand All @@ -127,7 +137,7 @@ export async function capabilitiesCommand(): Promise<CommandResult<CapabilitiesD
)
}

function getUnavailableReason(installer: 'brew' | 'bun' | 'cargo' | 'npm' | 'winget'): string {
function getUnavailableReason(installer: 'brew' | 'bun' | 'cargo' | 'npm' | 'pip' | 'winget'): string {
if (installer === 'winget' && process.platform !== 'win32') return 'not-on-platform'

if (installer === 'brew' && process.platform === 'win32') return 'not-on-platform'
Expand All @@ -148,6 +158,7 @@ function renderCapabilitiesHuman(result: { data?: CapabilitiesData }): void {
console.log(` npm: ${formatCapabilityAvailability(result.data.installers.npm)}`)
console.log(` brew: ${formatCapabilityAvailability(result.data.installers.brew)}`)
console.log(` cargo: ${formatCapabilityAvailability(result.data.installers.cargo)}`)
console.log(` pip: ${formatCapabilityAvailability(result.data.installers.pip)}`)
console.log(` winget: ${formatCapabilityAvailability(result.data.installers.winget)}`)

console.log(pc.bold('\n Features:'))
Expand Down
17 changes: 14 additions & 3 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { createSuccessResult, emitCommandResult } from '../output'
import { getSelfUpgradeRecoveryHintForInspection, inspectSelf } from '../self'
import { inspectRegisteredAgents } from '../services/agents'
import { pc } from '../utils/color'
import { isBrewAvailable, isBunAvailable, isCargoAvailable, isNpmAvailable, isWingetAvailable } from '../utils/detect'
import {
isBrewAvailable,
isBunAvailable,
isCargoAvailable,
isNpmAvailable,
isPipAvailable,
isWingetAvailable,
} from '../utils/detect'
import { isVersionNewer } from '../utils/version'

type DoctorIssueCategory = 'agent' | 'installers' | 'self'
Expand Down Expand Up @@ -49,6 +56,7 @@ interface DoctorData {
bun: boolean
cargo: boolean
npm: boolean
pip: boolean
winget: boolean
}
self: {
Expand All @@ -66,6 +74,7 @@ export async function doctorCommand(): Promise<CommandResult<DoctorData>> {
const npmAvailable = await isNpmAvailable()
const brewAvailable = await isBrewAvailable()
const cargoAvailable = await isCargoAvailable()
const pipAvailable = await isPipAvailable()
const wingetAvailable = await isWingetAvailable()

const selfInspection = await inspectSelf()
Expand All @@ -91,14 +100,14 @@ export async function doctorCommand(): Promise<CommandResult<DoctorData>> {
const troubleshootingDocsRef = 'docs/runbooks/quantex-troubleshooting.md'
const selfUpgradeDocsRef = 'docs/runbooks/release-and-self-upgrade-debugging.md'

if (!bunAvailable && !npmAvailable && !brewAvailable && !cargoAvailable && !wingetAvailable) {
if (!bunAvailable && !npmAvailable && !brewAvailable && !cargoAvailable && !pipAvailable && !wingetAvailable) {
issues.push({
blocking: true,
category: 'installers',
code: 'NO_MANAGED_INSTALLER',
docsRef: troubleshootingDocsRef,
message:
'No managed installer found. Install bun, npm, brew, cargo, or winget before relying on managed lifecycle operations.',
'No managed installer found. Install bun, npm, brew, cargo, pip, or winget before relying on managed lifecycle operations.',
severity: 'warning',
subject: { kind: 'system' },
suggestedAction: 'restore-managed-installer',
Expand Down Expand Up @@ -209,6 +218,7 @@ export async function doctorCommand(): Promise<CommandResult<DoctorData>> {
bun: bunAvailable,
cargo: cargoAvailable,
npm: npmAvailable,
pip: pipAvailable,
winget: wingetAvailable,
},
self: {
Expand Down Expand Up @@ -239,6 +249,7 @@ function renderDoctorHuman(result: { data?: DoctorData }): void {
console.log(` npm: ${result.data.installers.npm ? pc.green('available') : pc.red('not found')}`)
console.log(` brew: ${result.data.installers.brew ? pc.green('available') : pc.red('not found')}`)
console.log(` cargo: ${result.data.installers.cargo ? pc.green('available') : pc.red('not found')}`)
console.log(` pip: ${result.data.installers.pip ? pc.green('available') : pc.red('not found')}`)
console.log(` winget:${result.data.installers.winget ? pc.green('available') : pc.red('not found')}`)

console.log(`\n${pc.bold('Quantex CLI:')}`)
Expand Down
7 changes: 7 additions & 0 deletions src/package-manager/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ const INSTALLER_CAPABILITIES: Record<InstallType, InstallerCapabilities> = {
canUpdate: true,
lifecycle: 'managed',
},
pip: {
canInstall: true,
canLookupLatestVersion: false,
canUninstall: true,
canUpdate: true,
lifecycle: 'managed',
},
script: {
canInstall: true,
canLookupLatestVersion: false,
Expand Down
Loading
Loading