From 02d9872b95b7d6d3ef839297a9e5f636f7d4d722 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Fri, 12 Sep 2025 12:21:30 -0300 Subject: [PATCH] feat: implement MetaPluginExec for external command execution Co-authored-by: aider (openai/andrew/openrouter/sonoma-sky-alpha) --- src/meta_plugin/exec.rs | 224 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 2 deletions(-) diff --git a/src/meta_plugin/exec.rs b/src/meta_plugin/exec.rs index 03adc5b..2904d95 100644 --- a/src/meta_plugin/exec.rs +++ b/src/meta_plugin/exec.rs @@ -1,5 +1,5 @@ use log::*; -use std::io::Write; +use std::io::{self, Write}; use std::process::{Command, Stdio, Child}; use which::which; @@ -67,4 +67,224 @@ impl MetaPluginExec { /// # Examples /// /// ``` - /// let plugin \ No newline at end of file + /// let plugin = MetaPluginExec::new("date", &[], "date_output", false, None, None); + /// ``` + pub fn new( + program: &str, + args: &[String], + meta_name: String, + split_whitespace: bool, + _options: Option>, + outputs: Option>, + ) -> MetaPluginExec { + let supported = which(program).is_ok(); + + let mut base = BaseMetaPlugin::new(); + + // Set default output + let default_outputs = &[meta_name.as_str()]; + base.initialize_plugin(default_outputs, _options, outputs); + + MetaPluginExec { + program: program.to_string(), + args: args.to_vec(), + supported, + split_whitespace, + process: None, + writer: None, + result: None, + base, + } + } + + /// Starts the external process if not already running. + /// + /// Spawns the command with piped stdin/stdout and stores the child process and writer. + /// + /// # Returns + /// + /// * `MetaPluginResponse` - Empty response, initializes the process. + fn start_process(&mut self) -> MetaPluginResponse { + if self.process.is_some() { + return MetaPluginResponse { + metadata: Vec::new(), + is_finalized: false, + }; + } + + if !self.supported { + debug!("META: Exec plugin: program '{}' not supported", self.program); + return MetaPluginResponse { + metadata: Vec::new(), + is_finalized: true, + }; + } + + let mut cmd = Command::new(&self.program); + cmd.args(&self.args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + match cmd.spawn() { + Ok(child) => { + let stdin = child.stdin.unwrap(); + self.writer = Some(Box::new(stdin)); + self.process = Some(child); + debug!("META: Exec plugin: started process for '{}'", self.program); + MetaPluginResponse { + metadata: Vec::new(), + is_finalized: false, + } + } + Err(e) => { + error!("META: Exec plugin: failed to start '{}': {}", self.program, e); + MetaPluginResponse { + metadata: Vec::new(), + is_finalized: true, + } + } + } + } +} + +impl MetaPlugin for MetaPluginExec { + fn meta_type(&self) -> MetaPluginType { + MetaPluginType::Exec + } + + fn is_supported(&self) -> bool { + self.supported + } + + fn is_internal(&self) -> bool { + false + } + + fn initialize(&mut self) -> MetaPluginResponse { + self.start_process() + } + + fn update(&mut self, data: &[u8]) -> MetaPluginResponse { + if let Some(writer) = self.writer.as_mut() { + if let Err(e) = writer.write_all(data) { + error!("META: Exec plugin: failed to write to stdin: {}", e); + } + } + MetaPluginResponse { + metadata: Vec::new(), + is_finalized: false, + } + } + + fn finalize(&mut self) -> MetaPluginResponse { + let mut metadata = Vec::new(); + + // Close stdin if writer exists + drop(self.writer.take()); + + // Wait for process to complete and capture output + if let Some(mut child) = self.process.take() { + match child.wait_with_output() { + Ok(output) => { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let result = if self.split_whitespace { + stdout.split_whitespace().next().unwrap_or(&stdout).to_string() + } else { + stdout.trim().to_string() + }; + + self.result = Some(result.clone()); + + if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs( + self.base.outputs().keys().next().unwrap_or(&"exec".to_string()), + serde_yaml::Value::String(result), + self.base.outputs(), + ) { + metadata.push(meta_data); + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("META: Exec plugin: command failed: {}", stderr); + } + } + Err(e) => { + error!("META: Exec plugin: failed to wait on process: {}", e); + } + } + } + + MetaPluginResponse { + metadata, + is_finalized: true, + } + } + + fn program_info(&self) -> Option<(&str, Vec<&str>)> { + let args_str: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + Some((&self.program, args_str)) + } + + fn outputs(&self) -> &std::collections::HashMap { + &self.base.outputs + } + + fn outputs_mut(&mut self) -> &mut std::collections::HashMap { + &mut self.base.outputs + } + + fn options(&self) -> &std::collections::HashMap { + &self.base.options + } + + fn options_mut(&mut self) -> &mut std::collections::HashMap { + &mut self.base.options + } + + fn default_outputs(&self) -> Vec { + vec!["exec".to_string()] + } +} + +use crate::meta_plugin::register_meta_plugin; + +// Register the plugin at module initialization time +#[ctor::ctor] +fn register_exec_plugin() { + register_meta_plugin(MetaPluginType::Exec, |options, outputs| { + // Parse command from options for registration + let mut program_name = String::new(); + let mut args = Vec::new(); + let mut meta_name = "exec".to_string(); + let mut split_whitespace = false; + + if let Some(opts) = &options { + if let Some(command_value) = opts.get("command") + && let Some(command_str) = command_value.as_str() { + let parts: Vec<&str> = command_str.split_whitespace().collect(); + if !parts.is_empty() { + program_name = parts[0].to_string(); + args = parts[1..].iter().map(|s| s.to_string()).collect(); + } + } + if let Some(split_value) = opts.get("split_whitespace") + && let Some(split_bool) = split_value.as_bool() { + split_whitespace = split_bool; + } + if let Some(name_value) = opts.get("name") + && let Some(name_str) = name_value.as_str() { + meta_name = name_str.to_string(); + } + } + + Box::new(MetaPluginExec::new( + &program_name, + &args, + meta_name, + split_whitespace, + options, + outputs, + )) + }); +}