use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::{Command, Stdio};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command as AsyncCommand;
use tokio::sync::{broadcast, oneshot};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRequest {
pub config_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCommandRequest {
pub cmd: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteFileRequest {
pub path: String,
pub contents: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadFileRequest {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStartedResponse {
pub job_id: String,
pub config_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunOutputResponse {
pub job_id: String,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
}
pub fn resolve_run_config(
config_id: &str,
run_configs: &[crate::ide::orchestration::RunConfig],
) -> Option<(String, Vec<String>, String)> {
for cfg in run_configs {
if cfg.id == config_id {
let cmd = cfg.command.clone();
let args = cfg.args.clone().unwrap_or_default();
let cwd = cfg.cwd.clone().unwrap_or_else(|| ".".to_string());
return Some((cmd, args, cwd));
}
}
None
}
pub fn run_command_blocking(
cmd: &str,
args: &[String],
cwd: Option<&Path>,
) -> Result<(String, String, i32), String> {
let mut child = Command::new(cmd)
.args(args)
.current_dir(cwd.unwrap_or(Path::new(".")))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to spawn: {}", e))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "stdout not captured".to_string())?;
let stderr = child
.stderr
.take()
.ok_or_else(|| "stderr not captured".to_string())?;
let out = std::io::read_to_string(std::io::BufReader::new(stdout))
.map_err(|e| format!("Failed to read stdout: {}", e))?;
let err = std::io::read_to_string(std::io::BufReader::new(stderr))
.map_err(|e| format!("Failed to read stderr: {}", e))?;
let status = child.wait().map_err(|e| format!("Failed to wait: {}", e))?;
let code = status.code().unwrap_or(-1);
Ok((out, err, code))
}
pub fn spawn_run_streaming(
cmd: &str,
args: &[String],
cwd: Option<&Path>,
) -> Result<(broadcast::Sender<String>, oneshot::Sender<()>), String> {
let mut child = AsyncCommand::new(cmd)
.args(args)
.current_dir(cwd.unwrap_or(Path::new(".")))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to spawn: {}", e))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "stdout not captured".to_string())?;
let stderr = child
.stderr
.take()
.ok_or_else(|| "stderr not captured".to_string())?;
let (output_tx, _) = broadcast::channel::<String>(64);
let (kill_tx, kill_rx) = oneshot::channel::<()>();
let tx_out = output_tx.clone();
let tx_err = output_tx.clone();
let tx_done = output_tx.clone();
tokio::spawn(async move {
let h_stdout = tokio::spawn(async move {
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
let _ = tx_out.send(format!("{}\n", line));
}
});
let h_stderr = tokio::spawn(async move {
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
let _ = tx_err.send(format!("[stderr] {}\n", line));
}
});
tokio::select! {
_ = kill_rx => {
let _ = child.kill().await;
}
_ = async {
let _ = tokio::join!(h_stdout, h_stderr);
let _ = child.wait().await;
} => {}
}
let _ = tx_done.send("[DONE]".to_string());
});
Ok((output_tx, kill_tx))
}
pub fn dal_binary_path() -> Result<std::path::PathBuf, String> {
std::env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))
}