v0.2.3 : execution shell des commandes avec pipe (rustup, bun, uv) + rafraichissement du PATH + python3 satisfait python

This commit is contained in:
2026-08-15 16:42:47 -04:00
parent e513dd485d
commit 2be5f8e8b2
5 changed files with 119 additions and 32 deletions
Generated
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
[[package]]
name = "agent-manager"
version = "0.2.2"
version = "0.2.3"
dependencies = [
"anyhow",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-manager"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
license = "MIT"
+7 -13
View File
@@ -151,19 +151,13 @@ pub fn ensure_deps(app: &App, agent: &AgentDef, runner: &dyn Runner) -> Result<(
let proceed = app
.confirm(&format!("Run the detected installer for {}? -> {cmd}", os.name))?;
if proceed {
if let Ok(tokens) = shell_words::split(cmd) {
if !tokens.is_empty() {
let prog = tokens[0].clone();
let args: Vec<String> = tokens[1..].to_vec();
let empty = BTreeMap::new();
let out = runner.run(&prog, &args, &empty, None, false)?;
if out.code != 0 {
app.log.warn(&format!(
"installation of '{}' failed (exit code {})",
dep.name, out.code
));
}
}
let code = crate::toolchain::run_install_command(runner, cmd)?;
crate::toolchain::refresh_path();
if code != 0 {
app.log.warn(&format!(
"installation of '{}' failed (exit code {code})",
dep.name
));
}
}
}
+15 -16
View File
@@ -65,7 +65,12 @@ pub fn run(app: &App, runner: &dyn Runner) -> DoctorReport {
];
let os = crate::toolchain::OsInfo::detect();
for (tool, required) in tools {
match runner.which(tool) {
let found = if *tool == "python" {
runner.which("python3").or_else(|| runner.which("python"))
} else {
runner.which(tool)
};
match found {
Some(path) => {
let mut detail = path.display().to_string();
let empty = BTreeMap::new();
@@ -211,8 +216,8 @@ fn is_on_path(dir: &std::path::Path) -> bool {
pub fn install_missing_tools(app: &App, runner: &dyn Runner) -> Result<Vec<String>> {
let mut installed = Vec::new();
let os = crate::toolchain::OsInfo::detect();
for tool in ["node", "python", "cargo", "go", "bun", "uv", "docker"] {
if runner.which(tool).is_some() {
for tool in ["git", "node", "python", "cargo", "go", "bun", "uv", "docker"] {
if crate::toolchain::tool_is_present(tool, runner) {
continue;
}
let cmds = crate::toolchain::install_commands(tool, &os);
@@ -225,19 +230,13 @@ pub fn install_missing_tools(app: &App, runner: &dyn Runner) -> Result<Vec<Strin
))? {
continue;
}
if let Ok(tokens) = shell_words::split(cmd) {
if !tokens.is_empty() {
let prog = tokens[0].clone();
let args: Vec<String> = tokens[1..].to_vec();
let empty = BTreeMap::new();
let out = runner.run(&prog, &args, &empty, None, false)?;
installed.push(if out.code == 0 {
format!("installed '{tool}' ({cmd})")
} else {
format!("failed to install '{tool}' (exit code {})", out.code)
});
}
}
let code = crate::toolchain::run_install_command(runner, cmd)?;
crate::toolchain::refresh_path();
installed.push(if code == 0 {
format!("installed '{tool}' ({cmd})")
} else {
format!("failed to install '{tool}' (exit code {code})")
});
}
Ok(installed)
}
+95 -1
View File
@@ -5,6 +5,10 @@
//! already present (scoop/winget on Windows, apt/dnf/pacman/apk on Linux,
//! brew on macOS) — and proposes the exact command to install it.
use crate::runner::Runner;
use anyhow::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Windows,
@@ -130,6 +134,66 @@ pub fn install_commands(tool: &str, os: &OsInfo) -> Vec<String> {
out
}
/// Run an installer command line. Commands containing shell syntax
/// (pipes, redirections, ...) are executed through the shell; plain
/// commands are spawned directly.
pub fn run_install_command(runner: &dyn Runner, cmd: &str) -> Result<i32> {
let needs_shell = cmd.contains('|')
|| cmd.contains('>')
|| cmd.contains('<')
|| cmd.contains('&')
|| cmd.contains(';');
let (prog, args) = if needs_shell {
#[cfg(windows)]
{
("cmd".to_string(), vec!["/C".to_string(), cmd.to_string()])
}
#[cfg(not(windows))]
{
("sh".to_string(), vec!["-c".to_string(), cmd.to_string()])
}
} else {
let tokens = shell_words::split(cmd)
.unwrap_or_else(|_| cmd.split_whitespace().map(String::from).collect());
if tokens.is_empty() {
return Ok(0);
}
(tokens[0].clone(), tokens[1..].to_vec())
};
let empty = std::collections::BTreeMap::new();
let out = runner.run(&prog, &args, &empty, None, false)?;
Ok(out.code)
}
/// After installers that modify ~/.profile or ~/.zshrc (rustup, bun, uv),
/// make the newly installed tools visible to the current process so the
/// dependency re-check can find them.
pub fn refresh_path() {
let mut extra: Vec<PathBuf> = Vec::new();
if let Some(home) = crate::config::home_dir() {
extra.push(home.join(".cargo").join("bin"));
extra.push(home.join(".bun").join("bin"));
extra.push(home.join(".local").join("bin"));
}
let mut parts: Vec<PathBuf> = extra.into_iter().filter(|p| p.is_dir()).collect();
if let Some(current) = std::env::var_os("PATH") {
parts.extend(std::env::split_paths(&current));
}
if let Ok(joined) = std::env::join_paths(parts) {
std::env::set_var("PATH", joined);
}
}
/// Is a tool considered present? Python is satisfied by python3 too
/// (Debian ships python3 without a python symlink).
pub fn tool_is_present(tool: &str, runner: &dyn Runner) -> bool {
if tool == "python" {
runner.which("python3").is_some() || runner.which("python").is_some()
} else {
runner.which(tool).is_some()
}
}
fn scoop_command(tool: &str) -> Option<String> {
let bucket = match tool {
"git" => "git",
@@ -208,7 +272,7 @@ fn linux_command(tool: &str, pkg: &str) -> Option<String> {
("pip", "apk") => "sudo apk add py3-pip",
("pip", "zypper") => "sudo zypper install -y python3-pip",
("cargo" | "rust", "apt") => {
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
}
("cargo" | "rust", "dnf") => "sudo dnf install -y rust cargo",
("cargo" | "rust", "pacman") => "sudo pacman -S --noconfirm rust",
@@ -278,6 +342,36 @@ mod tests {
assert!(brew_command("docker").unwrap().contains("--cask"));
}
#[test]
fn pipe_commands_run_through_shell() {
let runner = crate::runner::MockRunner::new();
let code = run_install_command(&runner, "curl -sSf https://x | sh").unwrap();
assert_eq!(code, 0);
let calls = runner.snapshot();
assert_eq!(calls.len(), 1);
if cfg!(windows) {
assert!(calls[0].starts_with("cmd /C "));
} else {
assert!(calls[0].starts_with("sh -c "));
}
}
#[test]
fn plain_commands_spawn_directly() {
let runner = crate::runner::MockRunner::new();
run_install_command(&runner, "apt install -y git").unwrap();
let calls = runner.snapshot();
assert!(calls[0].starts_with("apt install -y git"));
}
#[test]
fn python3_satisfies_python() {
let runner = crate::runner::MockRunner::new().with_found("python3");
assert!(tool_is_present("python", &runner));
let runner = crate::runner::MockRunner::new();
assert!(!tool_is_present("python", &runner));
}
#[test]
fn os_aware_selection() {
// Windows without scoop falls back to winget.