use std::path::Path;
use std::process::Command;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
impl CommandOutput {
pub fn success(&self) -> bool {
self.status == 0
}
}
pub trait CommandRunner: Send + Sync {
fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SystemRunner;
impl CommandRunner for SystemRunner {
fn run(&self, program: &str, args: &[String], cwd: Option<&Path>) -> Result<CommandOutput> {
let mut command = Command::new(program);
command.args(args);
if let Some(dir) = cwd {
command.current_dir(dir);
}
let output = command.output().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Error::CommandNotFound(program.to_string())
} else {
Error::Io(e)
}
})?;
Ok(CommandOutput {
status: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
}
pub fn parse_program_version(stdout: &str) -> Option<semver::Version> {
let mut parts = stdout.split_whitespace();
let _program = parts.next()?;
let version = parts.next()?;
semver::Version::parse(version).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_version_line() {
let v =
parse_program_version("rustc 1.99.0 (5f94df478 2026-08-27)").expect("version parsed");
assert_eq!(v, semver::Version::new(1, 99, 0));
}
#[test]
fn version_parse_rejects_garbage() {
assert!(parse_program_version("no version here").is_none());
assert!(parse_program_version("").is_none());
}
}