Skip to content
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ $ meowda deactivate
# Recreate environment (clear existing packages)
$ meowda create my-env -p 3.12 --clear

# Fork from the current Python environment
$ meowda fork cloned-env

# Fork from another managed environment
$ meowda fork cloned-tools --from tools

# Fork from any external virtual environment or Python executable
$ meowda fork cloned-ci --from /path/to/.venv
$ meowda fork cloned-system --from /path/to/python

# Install specific versions or from requirements
$ meowda install "django>=4.0,<5.0" "pytest==7.4.0"
$ meowda install -r requirements.txt
Expand Down Expand Up @@ -143,6 +153,8 @@ Add to your `settings.json`:
**Environment Management**

- `meowda create <name> -p <version>` - Create environment
- `meowda fork <name>` - Fork from the current active environment
- `meowda fork <name> --from <env|path>` - Fork from another managed environment or any Python environment path/executable
- `meowda activate <name>` - Activate environment
- `meowda deactivate` - Deactivate current environment
- `meowda remove <name>` - Remove environment
Expand Down
32 changes: 21 additions & 11 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub struct Args {
pub enum Commands {
#[clap(about = "Create a new virtual environment")]
Create(CreateArgs),
#[clap(about = "Fork a virtual environment from an existing environment or Python executable")]
Fork(ForkArgs),
#[clap(about = "Remove a virtual environment")]
Remove(RemoveArgs),
#[command(subcommand)]
Expand Down Expand Up @@ -53,21 +55,27 @@ pub enum Commands {

#[derive(Debug, Parser, PartialEq)]
pub struct CreateArgs {
#[arg(help = "Name of the virtual environment")]
pub name: String,
#[arg(short, long, help = "Python version/path to use (default: 3.13)")]
pub python: Option<String>,
#[arg(short, long, help = "Clear existing virtual environment")]
pub clear: bool,
#[clap(flatten)]
pub scope: ScopeArgs,
}

#[derive(Debug, Parser, PartialEq)]
pub struct ForkArgs {
#[arg(help = "Name of the virtual environment")]
pub name: String,
#[arg(
short,
long,
default_value = "3.13",
help = "Python version/path to use"
)]
pub python: String,
#[arg(
short,
long,
default_value = "false",
help = "Clear existing virtual environment"
long = "from",
value_name = "SOURCE",
help = "Fork from a managed environment name, a virtual environment path, or a Python executable (defaults to the current active Python environment, or the default Python if none is active)"
)]
pub source: Option<String>,
#[arg(short, long, help = "Clear existing virtual environment")]
pub clear: bool,
#[clap(flatten)]
pub scope: ScopeArgs,
Expand All @@ -91,6 +99,8 @@ pub struct InitArgs {
pub enum EnvCommandsArgs {
#[clap(about = "Create a new virtual environment")]
Create(CreateArgs),
#[clap(about = "Fork a virtual environment from an existing environment or Python executable")]
Fork(ForkArgs),
#[clap(about = "Remove a virtual environment")]
Remove(RemoveArgs),
#[clap(about = "List all virtual environments")]
Expand Down
27 changes: 17 additions & 10 deletions src/cli/env.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
use crate::backend::{EnvInfo, VenvBackend};
use crate::cli::args::{CreateArgs, DirArgs, ListArgs, RemoveArgs};
use crate::store::venv_store::{ScopeType, VenvScope, VenvStore};
use crate::venv::{CreateOptions, EnvInfo, VenvService};
use anstream::println;
use anyhow::Result;
use owo_colors::OwoColorize;

pub async fn create(args: CreateArgs, backend: &VenvBackend) -> Result<()> {
pub async fn create(args: CreateArgs, venv_service: &VenvService) -> Result<()> {
let scope_type = args.scope.try_into_scope_type()?;
let store = VenvStore::from_scope_type(scope_type)?;
store.init_if_needed()?;
backend
.create(&store, &args.name, &args.python, args.clear)
venv_service
.create(
&store,
&args.name,
CreateOptions {
python: args.python.as_deref(),
clear: args.clear,
},
)
.await?;
println!("Virtual environment '{}' created successfully.", args.name);
Ok(())
}

pub async fn remove(args: RemoveArgs, backend: &VenvBackend) -> Result<()> {
pub async fn remove(args: RemoveArgs, venv_service: &VenvService) -> Result<()> {
let scope_type = args.scope.try_into_scope_type()?;
let detected_venv_scope = crate::cli::utils::search_venv(scope_type, &args.name)?;
let store = VenvStore::from_specified_scope(detected_venv_scope)?;
Expand All @@ -26,7 +33,7 @@ pub async fn remove(args: RemoveArgs, backend: &VenvBackend) -> Result<()> {
args.name
);
}
backend.remove(&store, &args.name).await?;
venv_service.remove(&store, &args.name).await?;
println!("Virtual environment '{}' removed successfully.", args.name);
Ok(())
}
Expand Down Expand Up @@ -59,8 +66,8 @@ fn show_envs(envs: &[EnvInfo], shadowed_names: &[String]) -> Result<()> {
Ok(())
}

pub async fn list(args: ListArgs, backend: &VenvBackend) -> Result<()> {
let all_envs = backend.list().await?;
pub async fn list(args: ListArgs, venv_service: &VenvService) -> Result<()> {
let all_envs = venv_service.list().await?;
let scope_type = args.scope.try_into_scope_type()?;
let show_local = matches!(scope_type, ScopeType::Local | ScopeType::Unspecified);
let show_global = matches!(scope_type, ScopeType::Global | ScopeType::Unspecified);
Expand Down Expand Up @@ -89,10 +96,10 @@ pub async fn list(args: ListArgs, backend: &VenvBackend) -> Result<()> {
Ok(())
}

pub async fn dir(args: DirArgs, backend: &VenvBackend) -> Result<()> {
pub async fn dir(args: DirArgs, venv_service: &VenvService) -> Result<()> {
let scope_type = args.scope.try_into_scope_type()?;
let store = VenvStore::from_scope_type(scope_type)?;
let path = backend.dir(&store)?;
let path = venv_service.dir(&store)?;
println!("{}", path.display());
Ok(())
}
24 changes: 24 additions & 0 deletions src/cli/fork.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::cli::args::ForkArgs;
use crate::store::venv_store::VenvStore;
use crate::venv::{ForkOptions, VenvService};
use anstream::println;
use anyhow::Result;

pub async fn fork(args: ForkArgs, venv_service: &VenvService) -> Result<()> {
let scope_type = args.scope.try_into_scope_type()?;
let store = VenvStore::from_scope_type(scope_type)?;
store.init_if_needed()?;
venv_service
.fork(
&store,
&args.name,
ForkOptions {
scope_type,
source: args.source.as_deref(),
clear: args.clear,
},
)
.await?;
println!("Virtual environment '{}' forked successfully.", args.name);
Ok(())
}
10 changes: 5 additions & 5 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use crate::backend::VenvBackend;
use crate::cli::args::{InstallArgs, UninstallArgs};
use crate::venv::VenvService;
use anyhow::Result;

pub async fn install(args: InstallArgs, backend: &VenvBackend) -> Result<()> {
pub async fn install(args: InstallArgs, venv_service: &VenvService) -> Result<()> {
let extra_args: Vec<&str> = args.extra_args.iter().map(|s| s.as_str()).collect();
backend.install(&extra_args).await?;
venv_service.install(&extra_args).await?;
Ok(())
}

pub async fn uninstall(args: UninstallArgs, backend: &VenvBackend) -> Result<()> {
pub async fn uninstall(args: UninstallArgs, venv_service: &VenvService) -> Result<()> {
let extra_args: Vec<&str> = args.extra_args.iter().map(|s| s.as_str()).collect();
backend.uninstall(&extra_args).await?;
venv_service.uninstall(&extra_args).await?;
Ok(())
}
10 changes: 5 additions & 5 deletions src/cli/link.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use crate::backend::VenvBackend;
use crate::cli::args::{LinkArgs, UnlinkArgs};
use crate::venv::VenvService;
use anyhow::Result;

pub async fn link(args: LinkArgs, backend: &VenvBackend) -> Result<()> {
backend.link(&args.name, &args.path).await?;
pub async fn link(args: LinkArgs, venv_service: &VenvService) -> Result<()> {
venv_service.link(&args.name, &args.path).await?;
Ok(())
}

pub async fn unlink(args: UnlinkArgs, backend: &VenvBackend) -> Result<()> {
backend.unlink(&args.name).await?;
pub async fn unlink(args: UnlinkArgs, venv_service: &VenvService) -> Result<()> {
venv_service.unlink(&args.name).await?;
Ok(())
}
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod activate;
pub mod args;
pub mod env;
pub mod fork;
pub mod init;
pub mod install;
pub mod link;
Expand Down
38 changes: 25 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
mod backend;
mod cli;
mod envs;
mod store;
mod venv;
use anstream::eprintln;
use clap::Parser;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = cli::args::Args::parse();
let backend = match backend::VenvBackend::new() {
Ok(backend) => backend,
let venv_service = match venv::VenvService::new() {
Ok(venv_service) => venv_service,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};

let result = match args.command {
cli::args::Commands::Create(create_args) => cli::env::create(create_args, &backend).await,
cli::args::Commands::Remove(remove_args) => cli::env::remove(remove_args, &backend).await,
cli::args::Commands::Create(create_args) => {
cli::env::create(create_args, &venv_service).await
}
cli::args::Commands::Fork(fork_args) => cli::fork::fork(fork_args, &venv_service).await,
cli::args::Commands::Remove(remove_args) => {
cli::env::remove(remove_args, &venv_service).await
}
cli::args::Commands::Env(env_args) => match env_args {
cli::args::EnvCommandsArgs::Create(create_args) => {
cli::env::create(create_args, &backend).await
cli::env::create(create_args, &venv_service).await
}
cli::args::EnvCommandsArgs::Fork(fork_args) => {
cli::fork::fork(fork_args, &venv_service).await
}
cli::args::EnvCommandsArgs::Remove(remove_args) => {
cli::env::remove(remove_args, &backend).await
cli::env::remove(remove_args, &venv_service).await
}
cli::args::EnvCommandsArgs::List(list_args) => {
cli::env::list(list_args, &backend).await
cli::env::list(list_args, &venv_service).await
}
cli::args::EnvCommandsArgs::Dir(dir_args) => {
cli::env::dir(dir_args, &venv_service).await
}
cli::args::EnvCommandsArgs::Dir(dir_args) => cli::env::dir(dir_args, &backend).await,
},
cli::args::Commands::Init(init_args) => cli::init::init(init_args).await,
cli::args::Commands::_GenerateInitScript => cli::init::generate_init_script().await,
Expand All @@ -41,13 +51,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
cli::activate::detect_activate_venv_path(activate_args).await
}
cli::args::Commands::Install(install_args) => {
cli::install::install(install_args, &backend).await
cli::install::install(install_args, &venv_service).await
}
cli::args::Commands::Uninstall(uninstall_args) => {
cli::install::uninstall(uninstall_args, &backend).await
cli::install::uninstall(uninstall_args, &venv_service).await
}
cli::args::Commands::Link(link_args) => cli::link::link(link_args, &venv_service).await,
cli::args::Commands::Unlink(unlink_args) => {
cli::link::unlink(unlink_args, &venv_service).await
}
cli::args::Commands::Link(link_args) => cli::link::link(link_args, &backend).await,
cli::args::Commands::Unlink(unlink_args) => cli::link::unlink(unlink_args, &backend).await,
};

if let Err(e) = result {
Expand Down
1 change: 1 addition & 0 deletions src/store/venv_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn get_candidate_scopes(scope_type: ScopeType) -> Result<Vec<VenvScope>> {
Ok(scopes)
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScopeType {
Local,
Global,
Expand Down
33 changes: 33 additions & 0 deletions src/venv/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;

pub(super) fn create_uv_venv(
uv_path: &str,
venv_path: &Path,
python: &str,
seed: bool,
include_system_site_packages: bool,
) -> Result<()> {
let venv_path_str = venv_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid path for virtual environment"))?;

let mut command = Command::new(uv_path);
command.args(["venv", venv_path_str, "--python", python]);
if seed {
command.arg("--seed");
}
if include_system_site_packages {
command.arg("--system-site-packages");
}

let status = command.status().context("Failed to execute uv command")?;
if !status.success() {
anyhow::bail!(
"Failed to create virtual environment. Check Python version/source environment and try again"
);
}

Ok(())
}
Loading