From 637932297ad43f296977184184caae0cd8dfa3bf Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 09:39:49 +0800 Subject: [PATCH 1/3] feat(pkg): add integrated package management commands Add package management commands that work with activated environments: - uvup add: add packages to active environment (supports --group) - uvup remove: remove packages from active environment (supports --group) - uvup lock: update lockfile of active environment (supports --upgrade) - uvup tree: display dependency tree of active environment (supports --depth) All commands use `uv --project` internally, enabling management from any directory. Added NoActiveEnvironment error type for better error messages when commands are used without an activated environment. --- CHANGELOG.md | 34 ++++++++ README.md | 36 +++++--- docs/COMMANDS.md | 193 ++++++++++++++++++++++++++++++++++++++--- docs/USE_CASES.md | 66 +++++++------- src/cli.rs | 36 +++++++- src/commands/add.rs | 37 ++++++++ src/commands/delete.rs | 19 ++++ src/commands/lock.rs | 34 ++++++++ src/commands/mod.rs | 4 + src/commands/remove.rs | 37 +++++--- src/commands/tree.rs | 34 ++++++++ src/error.rs | 5 ++ src/main.rs | 6 +- 13 files changed, 470 insertions(+), 71 deletions(-) create mode 100644 src/commands/add.rs create mode 100644 src/commands/delete.rs create mode 100644 src/commands/lock.rs create mode 100644 src/commands/tree.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2209b..4329588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.1] - 2025-11-17 + +### Added + +- **Package Management Commands**: Integrated package management with environment activation + - `uvup add ` - Add packages to active environment (supports `--group`) + - `uvup remove ` - Remove packages from active environment (supports `--group`) + - `uvup lock` - Update lockfile of active environment (supports `--upgrade`) + - `uvup tree` - Display dependency tree of active environment (supports `--depth`) +- All package commands use `uv --project` internally, allowing management from any directory + +### Changed + +- **Breaking**: Renamed `uvup remove ` to `uvup delete ` for environment deletion + - This clarifies the distinction between deleting environments vs removing packages + - `remove` is now exclusively for package removal + +### Documentation + +- Updated README.md with integrated package management examples +- Updated COMMANDS.md with complete package management command reference +- Updated USE_CASES.md replacing all `uv add`/`uv remove` with `uvup` equivalents +- Added design philosophy explanation for command organization + +### Technical + +- Added `commands/add.rs` for package addition +- Renamed `commands/remove.rs` to `commands/delete.rs` for environment deletion +- Created new `commands/remove.rs` for package removal +- Added `commands/lock.rs` for lockfile management +- Added `commands/tree.rs` for dependency tree display +- Added `NoActiveEnvironment` error type for better error messages + ## [0.2.0] - 2025-11-17 ### Added @@ -133,6 +166,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - clap 4.5 - CLI framework - dirs 6.0 - Cross-platform directory paths +[0.2.1]: https://github.com/KercyDing/uvup/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/KercyDing/uvup/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/KercyDing/uvup/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/KercyDing/uvup/compare/v0.1.1...v0.1.2 diff --git a/README.md b/README.md index 32c5861..cbad607 100644 --- a/README.md +++ b/README.md @@ -83,14 +83,14 @@ uvup list # Activate an environment uvup activate myproject -# Install packages (using uv) -uv add numpy pandas +# Add packages (using uvup proxy commands) +uvup add numpy pandas # Deactivate uvup deactivate -# Remove an environment -uvup remove myproject +# Delete an environment +uvup delete myproject ``` ### Environment Cloning @@ -199,20 +199,34 @@ uvup clone --help ## Scope -uvup focuses on **environment management and template-based workflows**. For package management, use uv directly: +uvup now provides **integrated environment and package management**: ```bash -# Environment management with uvup +# Environment management uvup create myproject +uvup list +uvup delete myproject +uvup clone source target + +# Activate environment uvup activate myproject -# Package management with uv -uv add numpy pandas -uv remove pandas -uv lock -uv sync +# Package management (requires activated environment) +uvup add numpy pandas +uvup add --group dev pytest black +uvup remove pandas +uvup lock +uvup lock --upgrade +uvup tree + +# Run commands directly (no uvup prefix needed) +python script.py +pytest +jupyter notebook ``` +**Key Design**: After activation, use `uvup` commands for package operations but run executables directly. + ## IDE Integration ### VSCode diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 942ac48..30f7f75 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -7,14 +7,22 @@ Complete reference for all uvup commands. - [Environment Management](#environment-management) - [create](#create) - [list](#list) - - [remove](#remove) -- [Environment Operations](#environment-operations) + - [delete](#delete) - [clone](#clone) +- [Project Management](#project-management) - [new](#new) - [sync](#sync) +- [Package Management](#package-management) + - [add](#add) + - [remove](#remove) + - [lock](#lock) + - [tree](#tree) - [System](#system) - [init](#init) - [update](#update) +- [Shell Commands](#shell-commands) + - [activate](#activate) + - [deactivate](#deactivate) ## Environment Management @@ -70,33 +78,31 @@ uvup list --- -### remove +### delete -Remove an existing environment. +Delete an existing environment. **Usage:** ```bash -uvup remove +uvup delete ``` **Arguments:** -- `` - Name of the environment to remove +- `` - Name of the environment to delete **Examples:** ```bash -uvup remove myproject +uvup delete myproject ``` **Notes:** - Permanently deletes the environment directory - Cannot be undone - Fails if environment doesn't exist -- Only one environment can be removed at a time +- Only one environment can be deleted at a time --- -## Environment Operations - ### clone Clone an existing environment to create an exact 1:1 copy. @@ -136,6 +142,8 @@ uvup clone production testing --- +## Project Management + ### new Create a new project from a template environment with modification support. @@ -307,6 +315,147 @@ To sync this project, run the same command without --dry-run --- +## Package Management + +All package management commands require an activated environment. They use `uv --project` internally, allowing you to manage packages from any directory. + +### add + +Add packages to the active environment. + +**Usage:** +```bash +uvup add [OPTIONS] +``` + +**Arguments:** +- `` - One or more packages to add + +**Options:** +- `--group ` - Add to optional dependency group + +**Examples:** +```bash +# Activate environment first +uvup activate myproject + +# Add packages +uvup add requests numpy pandas + +# Add with version specifiers +uvup add "requests>=2.28.0" "numpy<2.0" + +# Add to development group +uvup add --group dev pytest black mypy +``` + +**Notes:** +- Requires an activated environment +- Updates `pyproject.toml` and `uv.lock` +- Installs packages immediately +- Works from any directory (not just project root) + +--- + +### remove + +Remove packages from the active environment. + +**Usage:** +```bash +uvup remove [OPTIONS] +``` + +**Arguments:** +- `` - One or more packages to remove + +**Options:** +- `--group ` - Remove from optional dependency group + +**Examples:** +```bash +# Activate environment first +uvup activate myproject + +# Remove packages +uvup remove requests numpy + +# Remove from development group +uvup remove --group dev pytest +``` + +**Notes:** +- Requires an activated environment +- Updates `pyproject.toml` and `uv.lock` +- Uninstalls packages immediately +- Works from any directory (not just project root) + +--- + +### lock + +Update the lockfile of the active environment. + +**Usage:** +```bash +uvup lock [OPTIONS] +``` + +**Options:** +- `--upgrade` - Upgrade all packages to their latest compatible versions + +**Examples:** +```bash +# Activate environment first +uvup activate myproject + +# Update lock file +uvup lock + +# Upgrade all packages +uvup lock --upgrade +``` + +**Notes:** +- Requires an activated environment +- Updates `uv.lock` based on `pyproject.toml` +- Does not install packages (use `uv sync` to install) +- Works from any directory (not just project root) + +--- + +### tree + +Display the dependency tree of the active environment. + +**Usage:** +```bash +uvup tree [OPTIONS] +``` + +**Options:** +- `--depth ` - Maximum depth to display + +**Examples:** +```bash +# Activate environment first +uvup activate myproject + +# Show full dependency tree +uvup tree + +# Limit depth +uvup tree --depth 2 +``` + +**Notes:** +- Requires an activated environment +- Shows hierarchical view of dependencies +- Helps identify dependency conflicts +- Works from any directory (not just project root) + +--- + ## System ### init @@ -376,7 +525,7 @@ uvup update --check --- -## Shell-only Commands +## Shell Commands These commands are available only after running `uvup init`: @@ -397,6 +546,13 @@ uvup activate uvup activate myproject ``` +**Notes:** +- Sets `UVUP_ACTIVE_ENV` environment variable +- Modifies PATH to include environment's bin directory +- Enables package management commands (add/remove/lock/tree) + +--- + ### deactivate Deactivate the current virtual environment. @@ -411,6 +567,11 @@ uvup deactivate uvup deactivate ``` +**Notes:** +- Clears `UVUP_ACTIVE_ENV` environment variable +- Restores original PATH +- Disables package management commands + --- ## Command Decision Tree @@ -424,9 +585,15 @@ uvup deactivate - Current project from template → `sync` - uvup itself → `update` -**Need to manage?** +**Need to manage environments?** - See all environments → `list` -- Delete environment → `remove` +- Delete environment → `delete` + +**Need to manage packages?** (requires activation) +- Add packages → `add` +- Remove packages → `remove` +- Update lockfile → `lock` +- View dependencies → `tree` **Need to use?** - Enable activation → `init` diff --git a/docs/USE_CASES.md b/docs/USE_CASES.md index 9735ee9..2b73a6f 100644 --- a/docs/USE_CASES.md +++ b/docs/USE_CASES.md @@ -29,7 +29,7 @@ uvup create my-first-project --python 3.12 # 5. Activate and use it uvup activate my-first-project -uv add requests numpy +uvup add requests numpy python -c "import requests; print(requests.__version__)" uvup deactivate ``` @@ -40,7 +40,7 @@ uvup deactivate # Create a simple data science project uvup create ds-project uvup activate ds-project -uv add numpy pandas matplotlib jupyter +uvup add numpy pandas matplotlib jupyter jupyter notebook ``` @@ -54,22 +54,22 @@ Create reusable templates for different project types: # 1. Create base web template uvup create web-template uvup activate web-template -uv add fastapi uvicorn pydantic -uv add --group dev pytest black mypy ruff +uvup add fastapi uvicorn pydantic +uvup add --group dev pytest black mypy ruff uvup deactivate # 2. Create data science template uvup create ds-template uvup activate ds-template -uv add numpy pandas matplotlib seaborn scikit-learn -uv add --group dev jupyter ipykernel pytest +uvup add numpy pandas matplotlib seaborn scikit-learn +uvup add --group dev jupyter ipykernel pytest uvup deactivate # 3. Create CLI tool template uvup create cli-template uvup activate cli-template -uv add click rich typer -uv add --group dev pytest pytest-cov +uvup add click rich typer +uvup add --group dev pytest pytest-cov uvup deactivate ``` @@ -109,9 +109,9 @@ uvup sync --template web-template --exclude pytest,black,mypy # Team lead creates template uvup create team-base-template --python 3.11 uvup activate team-base-template -uv add requests httpx pydantic sqlalchemy -uv add --group dev pytest black mypy ruff pre-commit -uv add --group docs sphinx sphinx-rtd-theme +uvup add requests httpx pydantic sqlalchemy +uvup add --group dev pytest black mypy ruff pre-commit +uvup add --group docs sphinx sphinx-rtd-theme uvup deactivate # Team lead shares the pyproject.toml @@ -187,7 +187,7 @@ uvup clone production-env experiment-env # Activate and test new packages uvup activate experiment-env -uv add experimental-package +uvup add experimental-package python -m pytest # ... experiment ... uvup deactivate @@ -198,7 +198,7 @@ uvup sync --template production-template --dry-run uvup sync --template production-template # If failed, just remove experiment -uvup remove experiment-env +uvup delete experiment-env ``` ### Upgrading Dependencies @@ -209,20 +209,20 @@ uvup clone my-project my-project-backup # Upgrade in original uvup activate my-project -uv add numpy@latest pandas@latest +uvup add numpy@latest pandas@latest python -m pytest # ... verify everything works ... uvup deactivate # If successful, update template cd ~/.uvup/my-project-template -uv add numpy@latest pandas@latest +uvup add numpy@latest pandas@latest uv lock # If failed, restore from backup -uvup remove my-project +uvup delete my-project uvup clone my-project-backup my-project -uvup remove my-project-backup +uvup delete my-project-backup ``` ## Advanced Patterns @@ -235,19 +235,19 @@ uvup remove my-project-backup # Base template with core dependencies uvup create base-template uvup activate base-template -uv add fastapi uvicorn pydantic sqlalchemy +uvup add fastapi uvicorn pydantic sqlalchemy uvup deactivate # Development environment (full stack) uvup new my-project-dev --template base-template cd my-project-dev -uv add --group dev pytest black mypy ruff ipython -uv add --group dev pytest-cov pytest-asyncio +uvup add --group dev pytest black mypy ruff ipython +uvup add --group dev pytest-cov pytest-asyncio # Testing environment (dev + test tools only) uvup new my-project-test --template base-template cd ../my-project-test -uv add --group dev pytest pytest-cov pytest-asyncio +uvup add --group dev pytest pytest-cov pytest-asyncio # Production environment (minimal) uvup new my-project-prod --template base-template \ @@ -262,22 +262,22 @@ uvup new my-project-prod --template base-template \ # Create shared base template uvup create microservice-base uvup activate microservice-base -uv add fastapi uvicorn pydantic httpx -uv add --group dev pytest pytest-asyncio +uvup add fastapi uvicorn pydantic httpx +uvup add --group dev pytest pytest-asyncio uvup deactivate # Create service-specific projects uvup new auth-service --template microservice-base cd auth-service -uv add pyjwt passlib[bcrypt] python-multipart +uvup add pyjwt passlib[bcrypt] python-multipart uvup new user-service --template microservice-base cd ../user-service -uv add sqlalchemy asyncpg alembic +uvup add sqlalchemy asyncpg alembic uvup new notification-service --template microservice-base cd ../notification-service -uv add aiosmtplib jinja2 celery +uvup add aiosmtplib jinja2 celery ``` ### Educational Environments @@ -288,7 +288,7 @@ uv add aiosmtplib jinja2 celery # Instructor creates course template uvup create course-python-basics uvup activate course-python-basics -uv add ipython jupyter notebook pytest +uvup add ipython jupyter notebook pytest uvup deactivate # Students clone for each module @@ -298,10 +298,10 @@ uvup new module-03-classes --template course-python-basics # Each module can have specific packages cd module-02-functions -uv add hypothesis # for property-based testing +uvup add hypothesis # for property-based testing cd ../module-03-classes -uv add attrs pydantic # for class examples +uvup add attrs pydantic # for class examples ``` ### CI/CD Integration @@ -312,7 +312,7 @@ uv add attrs pydantic # for class examples # Local development uvup create ci-template uvup activate ci-template -uv add pytest pytest-cov black mypy ruff +uvup add pytest pytest-cov black mypy ruff uvup deactivate # Export for CI (Dockerfile) @@ -366,7 +366,7 @@ YAML # Base template uvup create app-template uvup activate app-template -uv add requests flask numpy +uvup add requests flask numpy uvup deactivate # Test with minimum versions @@ -380,7 +380,7 @@ uv run pytest # Test with latest versions uvup new app-latest-versions --template app-template cd ../app-latest-versions -uv add requests@latest flask@latest numpy@latest +uvup add requests@latest flask@latest numpy@latest uv run pytest # Test without optional dependencies @@ -428,7 +428,7 @@ for template in web-template ds-template cli-template; do if uvup list | grep -q "$template"; then echo "Updating $template..." cd ~/.uvup/$template - uv add --upgrade $(uv pip list --format=freeze | cut -d= -f1) + uvup add --upgrade $(uv pip list --format=freeze | cut -d= -f1) uv lock fi done diff --git a/src/cli.rs b/src/cli.rs index 25a7762..9509341 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,9 +26,9 @@ pub(crate) enum Commands { #[command(about = "List all environments")] List, - #[command(about = "Remove an environment")] - Remove { - #[arg(help = "Name of the environment to remove")] + #[command(about = "Delete an environment")] + Delete { + #[arg(help = "Name of the environment to delete")] name: String, }, @@ -104,4 +104,34 @@ pub(crate) enum Commands { #[arg(short, long, help = "Only check for updates without installing")] check: bool, }, + + #[command(about = "Add packages to the active environment")] + Add { + #[arg(help = "Packages to add", required = true)] + packages: Vec, + + #[arg(long, help = "Add to optional dependency group")] + group: Option, + }, + + #[command(about = "Remove packages from the active environment")] + Remove { + #[arg(help = "Packages to remove", required = true)] + packages: Vec, + + #[arg(long, help = "Remove from optional dependency group")] + group: Option, + }, + + #[command(about = "Update the lockfile of the active environment")] + Lock { + #[arg(long, help = "Update all packages to their latest versions")] + upgrade: bool, + }, + + #[command(about = "Display the dependency tree of the active environment")] + Tree { + #[arg(long, help = "Maximum depth to display")] + depth: Option, + }, } diff --git a/src/commands/add.rs b/src/commands/add.rs new file mode 100644 index 0000000..71c92ee --- /dev/null +++ b/src/commands/add.rs @@ -0,0 +1,37 @@ + +use crate::error::{Result, UvupError}; +use crate::env::paths::get_env_path; +use std::env; +use std::process::Command; + +pub(crate) fn run(packages: Vec, group: Option) -> Result<()> { + let active_env = env::var("UVUP_ACTIVE_ENV") + .map_err(|_| UvupError::NoActiveEnvironment)?; + + let env_path = get_env_path(&active_env)?; + + if !env_path.exists() { + return Err(UvupError::EnvNotFound(active_env)); + } + + let mut cmd = Command::new("uv"); + cmd.arg("--project").arg(&env_path).arg("add"); + + if let Some(g) = group { + cmd.arg("--group").arg(g); + } + + cmd.args(&packages); + + let status = cmd.status().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv add: {e}")) + })?; + + if status.success() { + Ok(()) + } else { + Err(UvupError::CommandExecutionFailed( + "uv add command failed".to_string(), + )) + } +} diff --git a/src/commands/delete.rs b/src/commands/delete.rs new file mode 100644 index 0000000..a03d2c4 --- /dev/null +++ b/src/commands/delete.rs @@ -0,0 +1,19 @@ +use crate::env::paths::{get_env_path, validate_env_name}; +use crate::error::{Result, UvupError}; +use crate::utils::print_success; +use std::fs; + +pub(crate) fn run(name: String) -> Result<()> { + validate_env_name(&name)?; + + let env_path = get_env_path(&name)?; + + if !env_path.exists() { + return Err(UvupError::EnvNotFound(name)); + } + + fs::remove_dir_all(&env_path)?; + + print_success(&format!("Environment '{name}' removed")); + Ok(()) +} diff --git a/src/commands/lock.rs b/src/commands/lock.rs new file mode 100644 index 0000000..6dfc846 --- /dev/null +++ b/src/commands/lock.rs @@ -0,0 +1,34 @@ +use crate::error::{Result, UvupError}; +use crate::env::paths::get_env_path; +use std::env; +use std::process::Command; + +pub(crate) fn run(upgrade: bool) -> Result<()> { + let active_env = env::var("UVUP_ACTIVE_ENV") + .map_err(|_| UvupError::NoActiveEnvironment)?; + + let env_path = get_env_path(&active_env)?; + + if !env_path.exists() { + return Err(UvupError::EnvNotFound(active_env)); + } + + let mut cmd = Command::new("uv"); + cmd.arg("--project").arg(&env_path).arg("lock"); + + if upgrade { + cmd.arg("--upgrade"); + } + + let status = cmd.status().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv lock: {e}")) + })?; + + if status.success() { + Ok(()) + } else { + Err(UvupError::CommandExecutionFailed( + "uv lock command failed".to_string(), + )) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ba7b32a..ea1ff43 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,8 +1,12 @@ +pub(crate) mod add; pub(crate) mod clone; pub(crate) mod create; +pub(crate) mod delete; pub(crate) mod init; pub(crate) mod list; +pub(crate) mod lock; pub(crate) mod new; pub(crate) mod remove; pub(crate) mod sync; +pub(crate) mod tree; pub(crate) mod update; diff --git a/src/commands/remove.rs b/src/commands/remove.rs index a03d2c4..a1e0f12 100644 --- a/src/commands/remove.rs +++ b/src/commands/remove.rs @@ -1,19 +1,36 @@ -use crate::env::paths::{get_env_path, validate_env_name}; use crate::error::{Result, UvupError}; -use crate::utils::print_success; -use std::fs; +use crate::env::paths::get_env_path; +use std::env; +use std::process::Command; -pub(crate) fn run(name: String) -> Result<()> { - validate_env_name(&name)?; +pub(crate) fn run(packages: Vec, group: Option) -> Result<()> { + let active_env = env::var("UVUP_ACTIVE_ENV") + .map_err(|_| UvupError::NoActiveEnvironment)?; - let env_path = get_env_path(&name)?; + let env_path = get_env_path(&active_env)?; if !env_path.exists() { - return Err(UvupError::EnvNotFound(name)); + return Err(UvupError::EnvNotFound(active_env)); } - fs::remove_dir_all(&env_path)?; + let mut cmd = Command::new("uv"); + cmd.arg("--project").arg(&env_path).arg("remove"); - print_success(&format!("Environment '{name}' removed")); - Ok(()) + if let Some(g) = group { + cmd.arg("--group").arg(g); + } + + cmd.args(&packages); + + let status = cmd.status().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv remove: {e}")) + })?; + + if status.success() { + Ok(()) + } else { + Err(UvupError::CommandExecutionFailed( + "uv remove command failed".to_string(), + )) + } } diff --git a/src/commands/tree.rs b/src/commands/tree.rs new file mode 100644 index 0000000..26ecd16 --- /dev/null +++ b/src/commands/tree.rs @@ -0,0 +1,34 @@ +use crate::error::{Result, UvupError}; +use crate::env::paths::get_env_path; +use std::env; +use std::process::Command; + +pub(crate) fn run(depth: Option) -> Result<()> { + let active_env = env::var("UVUP_ACTIVE_ENV") + .map_err(|_| UvupError::NoActiveEnvironment)?; + + let env_path = get_env_path(&active_env)?; + + if !env_path.exists() { + return Err(UvupError::EnvNotFound(active_env)); + } + + let mut cmd = Command::new("uv"); + cmd.arg("--project").arg(&env_path).arg("tree"); + + if let Some(d) = depth { + cmd.arg("--depth").arg(d.to_string()); + } + + let status = cmd.status().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv tree: {e}")) + })?; + + if status.success() { + Ok(()) + } else { + Err(UvupError::CommandExecutionFailed( + "uv tree command failed".to_string(), + )) + } +} diff --git a/src/error.rs b/src/error.rs index 984c5ae..e0d34e6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ pub(crate) enum UvupError { PathError(String), CommandExecutionFailed(String), UpdateFailed(String), + NoActiveEnvironment, } impl fmt::Display for UvupError { @@ -54,6 +55,10 @@ impl fmt::Display for UvupError { UvupError::UpdateFailed(msg) => { write!(f, "Update failed: {msg}") } + UvupError::NoActiveEnvironment => { + writeln!(f, "Error: No active environment")?; + write!(f, "Tip: Use 'uvup activate ' to activate an environment first") + } } } } diff --git a/src/main.rs b/src/main.rs index 862fc79..d751c6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ fn run() -> Result<()> { commands::create::run(name, python.as_deref())?; } Commands::List => commands::list::run()?, - Commands::Remove { name } => commands::remove::run(name)?, + Commands::Delete { name } => commands::delete::run(name)?, Commands::Clone { source, target } => commands::clone::run(source, target)?, Commands::New { name, @@ -59,6 +59,10 @@ fn run() -> Result<()> { dry_run, )?, Commands::Update { check } => commands::update::run(check)?, + Commands::Add { packages, group } => commands::add::run(packages, group)?, + Commands::Remove { packages, group } => commands::remove::run(packages, group)?, + Commands::Lock { upgrade } => commands::lock::run(upgrade)?, + Commands::Tree { depth } => commands::tree::run(depth)?, } Ok(()) From 78ddea4f0971deefec77fcd7273a4fda4234213a Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 09:46:14 +0800 Subject: [PATCH 2/3] test(integration): add tests for clone and package management --- tests/integration_test.rs | 135 ++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 13 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index ec2de8e..0e9e686 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -43,7 +43,7 @@ fn test_init_command() { } #[test] -fn test_create_list_remove_workflow() { +fn test_create_list_delete_workflow() { let test_env = "test-integration-env"; cleanup_test_env(test_env); @@ -69,14 +69,14 @@ fn test_create_list_remove_workflow() { let list_stdout = String::from_utf8_lossy(&list_output.stdout); assert!(list_stdout.contains(test_env)); - let remove_output = Command::new("cargo") - .args(["run", "--", "remove", test_env]) + let delete_output = Command::new("cargo") + .args(["run", "--", "delete", test_env]) .output() - .expect("Failed to execute uvup remove"); + .expect("Failed to execute uvup delete"); - assert!(remove_output.status.success()); - let remove_stdout = String::from_utf8_lossy(&remove_output.stdout); - assert!(remove_stdout.contains(&format!("Environment '{test_env}' removed"))); + assert!(delete_output.status.success()); + let delete_stdout = String::from_utf8_lossy(&delete_output.stdout); + assert!(delete_stdout.contains(&format!("Environment '{test_env}' removed"))); assert!(!env_path.exists()); } @@ -124,18 +124,18 @@ fn test_create_duplicate_environment() { } #[test] -fn test_remove_nonexistent_environment() { +fn test_delete_nonexistent_environment() { let test_env = "nonexistent-env"; cleanup_test_env(test_env); - let remove_output = Command::new("cargo") - .args(["run", "--", "remove", test_env]) + let delete_output = Command::new("cargo") + .args(["run", "--", "delete", test_env]) .output() - .expect("Failed to execute uvup remove"); + .expect("Failed to execute uvup delete"); - assert!(!remove_output.status.success()); - let stderr = String::from_utf8_lossy(&remove_output.stderr); + assert!(!delete_output.status.success()); + let stderr = String::from_utf8_lossy(&delete_output.stderr); assert!(stderr.contains("not found")); } @@ -181,3 +181,112 @@ fn test_list_empty_environments() { } } } + +// Package management tests +// Note: These tests require UVUP_ACTIVE_ENV to be set, which requires shell integration. +// They are marked as ignored by default and can be run with: cargo test -- --ignored + +#[test] +#[ignore] +fn test_add_package_without_activation() { + let add_output = Command::new("cargo") + .args(["run", "--", "add", "requests"]) + .output() + .expect("Failed to execute uvup add"); + + assert!(!add_output.status.success()); + let stderr = String::from_utf8_lossy(&add_output.stderr); + assert!(stderr.contains("No active environment")); +} + +#[test] +#[ignore] +fn test_remove_package_without_activation() { + let remove_output = Command::new("cargo") + .args(["run", "--", "remove", "requests"]) + .output() + .expect("Failed to execute uvup remove"); + + assert!(!remove_output.status.success()); + let stderr = String::from_utf8_lossy(&remove_output.stderr); + assert!(stderr.contains("No active environment")); +} + +#[test] +#[ignore] +fn test_lock_without_activation() { + let lock_output = Command::new("cargo") + .args(["run", "--", "lock"]) + .output() + .expect("Failed to execute uvup lock"); + + assert!(!lock_output.status.success()); + let stderr = String::from_utf8_lossy(&lock_output.stderr); + assert!(stderr.contains("No active environment")); +} + +#[test] +#[ignore] +fn test_tree_without_activation() { + let tree_output = Command::new("cargo") + .args(["run", "--", "tree"]) + .output() + .expect("Failed to execute uvup tree"); + + assert!(!tree_output.status.success()); + let stderr = String::from_utf8_lossy(&tree_output.stderr); + assert!(stderr.contains("No active environment")); +} + +#[test] +fn test_clone_environment() { + let source_env = "test-clone-source"; + let target_env = "test-clone-target"; + + cleanup_test_env(source_env); + cleanup_test_env(target_env); + + // Create source environment + Command::new("cargo") + .args(["run", "--", "create", source_env]) + .output() + .expect("Failed to create source environment"); + + // Clone it + let clone_output = Command::new("cargo") + .args(["run", "--", "clone", source_env, target_env]) + .output() + .expect("Failed to execute uvup clone"); + + assert!(clone_output.status.success()); + + // Verify target exists + let target_path = get_test_env_dir().join(target_env); + assert!(target_path.exists()); + assert!(target_path.join("pyproject.toml").exists()); + assert!(target_path.join(".venv").exists()); + + // Cleanup + cleanup_test_env(source_env); + cleanup_test_env(target_env); +} + +#[test] +fn test_clone_nonexistent_environment() { + let source_env = "nonexistent-source"; + let target_env = "test-clone-target-2"; + + cleanup_test_env(source_env); + cleanup_test_env(target_env); + + let clone_output = Command::new("cargo") + .args(["run", "--", "clone", source_env, target_env]) + .output() + .expect("Failed to execute uvup clone"); + + assert!(!clone_output.status.success()); + let stderr = String::from_utf8_lossy(&clone_output.stderr); + assert!(stderr.contains("not found") || stderr.contains("does not exist")); + + cleanup_test_env(target_env); +} From 55547f71f3e409bd91e55e4c4c8c7b8ec3e6e716 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 10:31:37 +0800 Subject: [PATCH 3/3] docs(book): migrate documentation to mdBook with GitHub Pages --- .github/workflows/deploy-docs.yml | 45 ++ .gitignore | 3 +- README.md | 207 ++------- book.toml | 28 ++ book/src/README.md | 50 +++ book/src/SUMMARY.md | 23 + book/src/architecture.md | 1 + book/src/changelog.md | 1 + book/src/commands/README.md | 70 +++ book/src/commands/environment.md | 135 ++++++ book/src/commands/package.md | 156 +++++++ book/src/commands/project.md | 184 ++++++++ book/src/commands/shell.md | 87 ++++ book/src/commands/system.md | 101 +++++ book/src/contributing/development.md | 1 + book/src/contributing/guidelines.md | 1 + book/src/core-concepts.md | 333 ++++++++++++++ book/src/installation.md | 100 +++++ book/src/quick-start.md | 180 ++++++++ book/src/roadmap.md | 1 + book/src/use-cases/README.md | 40 ++ book/src/use-cases/advanced.md | 331 ++++++++++++++ book/src/use-cases/collaboration.md | 255 +++++++++++ book/src/use-cases/development.md | 172 ++++++++ book/src/use-cases/getting-started.md | 156 +++++++ docs/COMMANDS.md | 601 -------------------------- docs/INSTALL.md | 189 -------- docs/UNINSTALL.md | 157 ------- docs/USE_CASES.md | 474 -------------------- src/commands/add.rs | 16 +- src/commands/lock.rs | 5 +- src/commands/remove.rs | 9 +- src/commands/tree.rs | 5 +- src/error.rs | 5 +- src/main.rs | 4 +- tests/integration_test.rs | 8 +- 36 files changed, 2511 insertions(+), 1623 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 book.toml create mode 100644 book/src/README.md create mode 100644 book/src/SUMMARY.md create mode 100644 book/src/architecture.md create mode 100644 book/src/changelog.md create mode 100644 book/src/commands/README.md create mode 100644 book/src/commands/environment.md create mode 100644 book/src/commands/package.md create mode 100644 book/src/commands/project.md create mode 100644 book/src/commands/shell.md create mode 100644 book/src/commands/system.md create mode 100644 book/src/contributing/development.md create mode 100644 book/src/contributing/guidelines.md create mode 100644 book/src/core-concepts.md create mode 100644 book/src/installation.md create mode 100644 book/src/quick-start.md create mode 100644 book/src/roadmap.md create mode 100644 book/src/use-cases/README.md create mode 100644 book/src/use-cases/advanced.md create mode 100644 book/src/use-cases/collaboration.md create mode 100644 book/src/use-cases/development.md create mode 100644 book/src/use-cases/getting-started.md delete mode 100644 docs/COMMANDS.md delete mode 100644 docs/INSTALL.md delete mode 100644 docs/UNINSTALL.md delete mode 100644 docs/USE_CASES.md diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..02fcf74 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,45 @@ +name: Deploy mdBook Documentation + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v2 + with: + mdbook-version: 'latest' + + - name: Build book + run: mdbook build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./book/build + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 0d049e3..981fbf5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ $RECYCLE.BIN/ !PULL_REQUEST_TEMPLATE.md !CHANGELOG.md !CONTRIBUTING.md -!docs/* +!book/** +book/build/ diff --git a/README.md b/README.md index cbad607..dfb1bd2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # uvup -[![DeepWiki](https://img.shields.io/badge/DeepWiki-blue)](https://deepwiki.com/KercyDing/uvup) [![uv](https://img.shields.io/badge/uv-black?logo=github)](https://github.com/astral-sh/uv) +[![Docs](https://img.shields.io/badge/Docs-blue)](https://kercyding.github.io/uvup/) [![DeepWiki](https://img.shields.io/badge/DeepWiki-orange)](https://deepwiki.com/KercyDing/uvup) [![uv](https://img.shields.io/badge/uv-black?logo=github)](https://github.com/astral-sh/uv) A conda-like environment manager for [uv](https://github.com/astral-sh/uv). @@ -27,212 +27,71 @@ curl -fsSL https://raw.githubusercontent.com/KercyDing/uvup/main/scripts/install Invoke-RestMethod https://raw.githubusercontent.com/KercyDing/uvup/main/scripts/install.ps1 | Invoke-Expression ``` -For detailed installation instructions, manual installation, and developer setup, see [Installation Guide](docs/INSTALL.md). +See the [Installation Guide](https://kercyding.github.io/uvup/installation.html) for more details. -## Uninstallation +## Quick Start -**Linux and macOS:** ```bash -curl -fsSL -O https://raw.githubusercontent.com/KercyDing/uvup/main/scripts/uninstall.sh -chmod +x uninstall.sh -./uninstall.sh -``` - -**Windows (PowerShell):** -```powershell -Invoke-WebRequest -Uri https://raw.githubusercontent.com/KercyDing/uvup/main/scripts/uninstall.ps1 -OutFile uninstall.ps1 -.\uninstall.ps1 -``` - -For detailed uninstallation instructions and manual removal, see [Uninstallation Guide](docs/UNINSTALL.md). - -## Planned Features - -### v0.2.0 - Completed - -- [x] `uvup clone ` - Clone environments (1:1 exact copy) -- [x] `uvup new --template