//! [`LocalProcessSpawner`] — local [`ProcessSpawner`] over `tokio::process` //! (ARCHITECTURE §5). //! //! Runs a **non-interactive** process to completion and captures //! stdout/stderr/exit code. Used by `DetectProfiles` (via [`CliAgentRuntime`]) //! and any future short-lived command (git fallbacks, scripts). Interactive //! processes go through the PTY port instead. use async_trait::async_trait; use tokio::process::Command; use domain::ports::{ExitStatus, Output, ProcessError, ProcessSpawner, SpawnSpec}; /// Process spawner backed by the local OS via `tokio::process::Command`. #[derive(Debug, Default, Clone, Copy)] pub struct LocalProcessSpawner; impl LocalProcessSpawner { /// Creates a new [`LocalProcessSpawner`]. #[must_use] pub const fn new() -> Self { Self } } #[async_trait] impl ProcessSpawner for LocalProcessSpawner { async fn run(&self, spec: SpawnSpec) -> Result { let mut cmd = Command::new(&spec.command); cmd.args(&spec.args); // `cwd` is "/" for detection probes; only set a real working directory // when the spec points at a concrete project path. if spec.cwd.as_str() != "/" { cmd.current_dir(spec.cwd.as_str()); } for (key, value) in &spec.env { cmd.env(key, value); } let output = cmd .output() .await .map_err(|e| ProcessError::Spawn(format!("{}: {e}", spec.command)))?; Ok(Output { status: ExitStatus { code: output.status.code(), }, stdout: output.stdout, stderr: output.stderr, }) } }