use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use anyhow::Result;
use serde::Serialize;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ApplyStatus {
Running,
Succeeded,
Failed,
}
#[derive(Debug, Clone, Serialize)]
pub struct ApplyRun {
pub id: String,
pub prompt: String,
pub status: ApplyStatus,
pub output: String,
pub exit_code: Option<i32>,
}
pub type ApplyRegistry = Arc<Mutex<HashMap<String, ApplyRun>>>;
pub fn new_registry() -> ApplyRegistry {
Arc::new(Mutex::new(HashMap::new()))
}
const MAX_OUTPUT: usize = 200_000;
pub async fn spawn(
prompt: String,
project_root: PathBuf,
registry: ApplyRegistry,
) -> Result<String> {
let exe = std::env::current_exe()?;
let id = format!("apply-{}", uuid::Uuid::new_v4().simple());
registry.lock().await.insert(
id.clone(),
ApplyRun {
id: id.clone(),
prompt: prompt.clone(),
status: ApplyStatus::Running,
output: String::new(),
exit_code: None,
},
);
let mut child = Command::new(exe)
.arg("run")
.arg(&prompt)
.arg("--yolo")
.current_dir(&project_root)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let reg = registry.clone();
let rid = id.clone();
tokio::spawn(async move {
tokio::join!(
pump_pipe(stdout, reg.clone(), rid.clone()),
pump_pipe(stderr, reg.clone(), rid.clone()),
);
let code = child.wait().await.ok().and_then(|s| s.code());
if let Some(run) = reg.lock().await.get_mut(&rid) {
run.exit_code = code;
run.status = if code == Some(0) {
ApplyStatus::Succeeded
} else {
ApplyStatus::Failed
};
}
});
Ok(id)
}
async fn pump_pipe<R>(pipe: Option<R>, reg: ApplyRegistry, rid: String)
where
R: tokio::io::AsyncRead + Unpin,
{
let Some(pipe) = pipe else { return };
let mut lines = BufReader::new(pipe).lines();
while let Ok(Some(line)) = lines.next_line().await {
if let Some(run) = reg.lock().await.get_mut(&rid) {
run.output.push_str(&line);
run.output.push('\n');
if run.output.len() > MAX_OUTPUT {
let cut = run.output.len() - MAX_OUTPUT;
run.output.drain(..cut);
}
}
}
}
pub async fn get(registry: &ApplyRegistry, id: &str) -> Option<ApplyRun> {
registry.lock().await.get(id).cloned()
}