- 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
131 lines
3.6 KiB
Rust
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()))
|
|
}
|