use crate::log::StyledText;
use crate::utils::{process_utils, terminal_utils};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::{
hash::{Hash, Hasher},
path::Path,
};
pub fn pre_commit_install(output_dir: &Path) -> Result<()> {
let args = vec!["run", "pre-commit", "install"];
CmdBuilder::uv(args).working_dir(output_dir).run()
}
pub fn add(packages: Vec<&str>) -> AddBuilder<'_> {
AddBuilder::new(packages)
}
pub fn remove(packages: Vec<&str>) -> CmdBuilder<'_> {
let mut args = vec!["remove"];
args.extend(packages.clone());
CmdBuilder::uv(args)
}
pub fn sync(python_version: Option<&str>) -> CmdBuilder<'_> {
let mut args = vec!["sync"];
if let Some(version) = python_version {
args.push("--python");
args.push(version);
}
CmdBuilder::uv(args)
}
pub fn show(package: &str) -> CmdBuilder<'_> {
let args = vec!["pip", "show", package];
CmdBuilder::uv(args)
}
pub fn reinstall(package: &str) -> Result<()> {
add(vec![package]).reinstall(true).run()
}
pub fn upgrade(packages: Vec<&str>) -> Result<()> {
let mut args = vec!["lock"];
args.extend(packages.iter().flat_map(|p| ["-P", p]));
CmdBuilder::uv(args).run()?;
sync(None).run()
}
pub async fn is_installed(package: &str) -> bool {
show(package).run_async().await.is_ok()
}
pub async fn self_version() -> Result<String> {
let args = vec!["self", "version", "--short"];
CmdBuilder::uv(args).run_async().await.map_err(|_| {
anyhow::anyhow!(concat!(
"uv not found. You can run\n\n",
" curl -LsSf https://astral.sh/uv/install.sh | sh\n\n",
"to install or get more information from https://astral.sh/blog/uv",
))
})
}
pub async fn list(outdated: bool) -> Result<Vec<Package>> {
let args: Vec<&str> = vec!["pip", "list", "--format=json"];
let mut builder = CmdBuilder::uv(args);
let stdout = if outdated {
builder
.arg("--outdated")
.timeout(300)
.run_async_with_spinner("Checking for outdated packages...")
.await?
} else {
builder.run_async().await?
};
Ok(serde_json::from_str(&stdout)?)
}
pub async fn show_package_info(package: &str, working_dir: Option<&Path>) -> Result<Package> {
let stdout = show(package)
.working_dir_opt(working_dir)
.run_async()
.await?;
let mut lines = stdout.lines();
let name = lines
.next()
.context("Failed to get package name from uv output")?
.trim_start_matches("Name: ")
.to_owned();
let version = lines
.next()
.context("Failed to get package version from uv output")?
.trim_start_matches("Version: ")
.to_owned();
let latest_version = None;
let location = Some(
lines
.next()
.context("Failed to get package location from uv output")?
.trim_start_matches("Location: ")
.to_owned(),
);
let requires = Some(
lines
.next()
.context("Failed to get package requirements from uv output")?
.trim_start_matches("Requires:")
.trim()
.split(", ")
.filter(|s: &&str| !s.is_empty())
.map(|s: &str| s.to_owned())
.collect::<Vec<String>>(),
);
let requires_by = Some(
lines
.next()
.context("Failed to get package required-by from uv output")?
.trim_start_matches("Required-by:")
.trim()
.split(", ")
.filter(|s: &&str| !s.is_empty())
.map(|s: &str| s.to_owned())
.collect::<Vec<String>>(),
);
Ok(Package {
name,
version,
latest_version,
location,
requires,
requires_by,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
pub struct Package {
pub name: String,
pub version: String,
pub latest_version: Option<String>,
pub location: Option<String>,
pub requires: Option<Vec<String>>,
pub requires_by: Option<Vec<String>>,
}
impl PartialEq for Package {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Hash for Package {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl Package {
pub fn is_outdated(&self) -> bool {
if let Some(latest_version) = self.latest_version.as_ref() {
&self.version != latest_version
} else {
false
}
}
pub fn display_info(&self) {
StyledText::new("")
.text(" ")
.cyan(&self.name)
.text(" ")
.green("v")
.green(&self.version)
.text(" ")
.with(|text| {
if let Some(v) = self.latest_version.as_ref()
&& self.is_outdated()
{
text.yellow(format!("(v{})", v));
}
})
.println();
}
}
pub struct CmdBuilder<'a> {
pub cmd: &'a str,
pub args: Vec<&'a str>,
pub working_dir: Option<&'a Path>,
pub timeout_secs: u16,
}
impl<'a> CmdBuilder<'a> {
pub fn uv(args: Vec<&'a str>) -> Self {
Self {
cmd: "uv",
args,
working_dir: None,
timeout_secs: 5,
}
}
pub fn quiet(&mut self) -> &mut Self {
self.args.insert(0, "--quiet");
self
}
pub fn arg(&mut self, arg: &'a str) -> &mut Self {
self.args.push(arg);
self
}
pub fn args(&mut self, args: Vec<&'a str>) -> &mut Self {
self.args.extend(args);
self
}
pub fn working_dir(&mut self, working_dir: &'a Path) -> &mut Self {
self.working_dir = Some(working_dir);
self
}
pub fn working_dir_opt(&mut self, working_dir: Option<&'a Path>) -> &mut Self {
self.working_dir = working_dir;
self
}
pub fn timeout(&mut self, timeout_secs: u16) -> &mut Self {
self.timeout_secs = timeout_secs;
self
}
pub fn run(&self) -> Result<()> {
process_utils::execute_interactive(self.cmd, &self.args, self.working_dir)
}
pub async fn run_async(&self) -> Result<String> {
let output = process_utils::execute_command_with_output(
self.cmd,
&self.args,
self.working_dir,
self.timeout_secs as u64,
)
.await?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub async fn run_async_with_spinner(&self, spinner_message: &str) -> Result<String> {
let spinner = terminal_utils::create_spinner(spinner_message);
let output = self.run_async().await?;
spinner.finish_and_clear();
Ok(output)
}
}
pub struct AddBuilder<'a> {
pub packages: Vec<&'a str>,
pub upgrade: bool,
pub index_url: Option<&'a str>,
pub working_dir: Option<&'a Path>,
pub extras: Option<Vec<&'a str>>,
pub reinstall: bool,
}
impl<'a> AddBuilder<'a> {
pub fn new(packages: Vec<&'a str>) -> Self {
Self {
packages,
upgrade: false,
index_url: None,
working_dir: None,
extras: None,
reinstall: false,
}
}
pub fn upgrade(&mut self, upgrade: bool) -> &mut Self {
self.upgrade = upgrade;
self
}
pub fn index_url_opt(&mut self, index_url: Option<&'a str>) -> &mut Self {
self.index_url = index_url;
self
}
pub fn index_url(&mut self, index_url: &'a str) -> &mut Self {
self.index_url = Some(index_url);
self
}
pub fn working_dir(&mut self, working_dir: &'a Path) -> &mut Self {
self.working_dir = Some(working_dir);
self
}
pub fn extras(&mut self, extras: Vec<&'a str>) -> &mut Self {
self.extras = Some(extras);
self
}
pub fn reinstall(&mut self, reinstall: bool) -> &mut Self {
self.reinstall = reinstall;
self
}
pub fn run(&self) -> Result<()> {
let mut args: Vec<&str> = vec!["add"];
args.extend(self.packages.clone());
if self.upgrade {
args.push("--upgrade");
}
if let Some(index_url) = self.index_url {
args.push("--index-url");
args.push(index_url);
}
if let Some(ref extras) = self.extras {
let extras = extras.iter().flat_map(|e| ["--extra", e]);
args.extend(extras);
}
if self.reinstall {
args.push("--reinstall");
}
process_utils::execute_interactive("uv", &args, self.working_dir)
}
}