v0.2.3 : execution shell des commandes avec pipe (rustup, bun, uv) + rafraichissement du PATH + python3 satisfait python
This commit is contained in:
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-manager"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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(¤t));
|
||||
}
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user