Files
agent-manager/src/installers/pipuv.rs
T
bruno 8ee3ddf3db agent-manager v0.2.0 : gestionnaire d'agents IA locaux
- CLI Rust (am) : list/install/uninstall/update/start/stop/restart/run/status/search/info/config/doctor/completion/export/import/self-update + shell interactif (Tab, historique)
- 9 methodes d'installation (npm, pip, uv, cargo, go, bun, curl, binary, git)
- catalogue de 72 agents, dependances, processus, securite (dry-run, checksums)
- sondes de version paralleles + cache (am list ~25 ms a chaud)
- installateurs one-liner : install.sh, install.ps1
2026-08-15 11:26:34 -04:00

131 lines
3.6 KiB
Rust

//! pip and uv installers: a private virtualenv per agent keeps everything
//! self-contained (no --user, no system pollution).
use super::*;
fn find_python(runner: &dyn Runner) -> Result<PathBuf> {
runner
.which("python3")
.or_else(|| runner.which("python"))
.ok_or_else(|| {
anyhow!(
"neither 'python3' nor 'python' found on PATH — install Python first (https://python.org)"
)
})
}
fn venv_bin_dir(venv: &Path) -> PathBuf {
if cfg!(windows) {
venv.join("Scripts")
} else {
venv.join("bin")
}
}
fn venv_python(venv: &Path) -> PathBuf {
venv_bin_dir(venv).join(if cfg!(windows) {
"python.exe"
} else {
"python"
})
}
/// Shared implementation for pip (use_uv=false) and uv (use_uv=true).
pub fn install(
app: &App,
agent: &AgentDef,
method: &MethodSpec,
runner: &dyn Runner,
use_uv: bool,
) -> Result<InstallOutcome> {
let pkg = method
.package
.as_deref()
.ok_or_else(|| anyhow!("pip/uv install requires 'package'"))?;
let python = find_python(runner)?;
let root = install_root(app, &agent.name);
let venv = root.join("venv");
ensure_dir(app, &root)?;
if !app.dry_run() && venv.exists() {
std::fs::remove_dir_all(&venv)
.with_context(|| format!("cannot remove {}", venv.display()))?;
}
let empty = BTreeMap::new();
sh(
runner,
&python.display().to_string(),
&[
"-m".to_string(),
"venv".to_string(),
venv.display().to_string(),
],
&empty,
None,
)?;
let vp = venv_python(&venv);
let vbin = venv_bin_dir(&venv);
if use_uv {
let uv = require_tool(
runner,
"uv",
"https://docs.astral.sh/uv — e.g. 'pip install uv'",
)?;
let mut args: Vec<String> = vec![
"pip".into(),
"install".into(),
"--python".into(),
vp.display().to_string(),
pkg.to_string(),
];
args.extend(method.args.iter().cloned());
sh(runner, &uv.display().to_string(), &args, &method.env, None)?;
} else {
let mut args: Vec<String> = vec![
"-m".into(),
"pip".into(),
"install".into(),
"--upgrade".into(),
pkg.to_string(),
];
args.extend(method.args.iter().cloned());
sh(runner, &vp.display().to_string(), &args, &method.env, None)?;
}
let expected = expected_bin_names(agent, method);
let mut bins = find_bins(&vbin, &expected);
if bins.is_empty() {
bins = find_bins(&bin_dir(app), &expected);
}
let version = bins.first().and_then(|b| detect_version(runner, b));
Ok(InstallOutcome { bins, version })
}
/// Latest published version from the PyPI JSON API.
pub fn latest(
_app: &App,
_agent: &AgentDef,
method: &MethodSpec,
_runner: &dyn Runner,
) -> Result<Option<String>> {
let Some(pkg) = method.package.as_deref() else {
return Ok(None);
};
let url = format!("https://pypi.org/pypi/{pkg}/json");
let resp = match ureq::get(&url)
.set("User-Agent", crate::download::USER_AGENT)
.call()
{
Ok(r) => r,
Err(_) => return Ok(None),
};
let Ok(text) = resp.into_string() else {
return Ok(None);
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
return Ok(None);
};
Ok(v.get("info")
.and_then(|i| i.get("version"))
.and_then(|x| x.as_str())
.map(|s| s.to_string()))
}