//! Build and run API — streams output via SSE. use std::convert::Infallible; use axum::{ extract::State, Json, response::{ sse::{Event, KeepAlive, Sse}, IntoResponse, }, }; use serde::Deserialize; use tokio::io::AsyncBufReadExt; use crate::{sse::classify_line, AppState}; // ── Request types ───────────────────────────────────────────────────────────── #[derive(Debug, Deserialize)] pub struct BuildRequest { /// Target: "debug" | "release" | "prod" #[serde(default = "default_target")] pub target: String, /// Optional specific file to build; otherwise builds whole project. pub file: Option, } #[derive(Debug, Deserialize)] pub struct RunRequest { pub file: String, } fn default_target() -> String { "debug".into() } // ── Handlers ────────────────────────────────────────────────────────────────── /// POST /api/build — streams build output as SSE. pub async fn build_handler( State(state): State, Json(req): Json, ) -> impl IntoResponse { let project_path = state.config.project_path.clone(); let stream = build_stream(project_path, req); Sse::new(stream).keep_alive(KeepAlive::default()) } /// POST /api/run — compiles and runs a file, streams output as SSE. pub async fn run_handler( State(state): State, Json(req): Json, ) -> impl IntoResponse { let project_path = state.config.project_path.clone(); let stream = run_stream(project_path, req); Sse::new(stream).keep_alive(KeepAlive::default()) } // ── Stream builders ─────────────────────────────────────────────────────────── fn build_stream( project_path: String, req: BuildRequest, ) -> impl tokio_stream::Stream> { async_stream::stream! { let el_bin = el_binary(); let mut cmd = tokio::process::Command::new(&el_bin); if let Some(ref file) = req.file { let file_path = format!("{project_path}/{file}"); cmd.arg("build").arg(&file_path).arg("--target").arg(&req.target); } else { let main = format!("{project_path}/src/main.el"); cmd.arg("build").arg(&main).arg("--target").arg(&req.target); } cmd.stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .current_dir(&project_path); yield Ok(Event::default().event("info").data(format!("Running: el build --target {}", req.target))); match cmd.spawn() { Err(e) => { yield Ok(Event::default().event("error").data(format!("Failed to start el: {e}"))); yield Ok(Event::default().event("done").data("1")); } Ok(mut child) => { if let Some(stdout) = child.stdout.take() { let mut reader = tokio::io::BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { yield Ok(classify_line(&line)); } } if let Some(stderr) = child.stderr.take() { let mut reader = tokio::io::BufReader::new(stderr).lines(); while let Ok(Some(line)) = reader.next_line().await { yield Ok(classify_line(&line)); } } let code = child.wait().await.map(|s| s.code().unwrap_or(0)).unwrap_or(1); yield Ok(Event::default().event("done").data(code.to_string())); } } } } fn run_stream( project_path: String, req: RunRequest, ) -> impl tokio_stream::Stream> { async_stream::stream! { let el_bin = el_binary(); let file_path = format!("{project_path}/{}", req.file); yield Ok(Event::default().event("info").data(format!("Running: el run {}", req.file))); let mut cmd = tokio::process::Command::new(&el_bin); cmd.arg("run") .arg(&file_path) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .current_dir(&project_path); match cmd.spawn() { Err(e) => { yield Ok(Event::default().event("error").data(format!("Failed to start el: {e}"))); yield Ok(Event::default().event("done").data("1")); } Ok(mut child) => { if let Some(stdout) = child.stdout.take() { let mut reader = tokio::io::BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { yield Ok(Event::default().event("output").data(line)); } } if let Some(stderr) = child.stderr.take() { let mut reader = tokio::io::BufReader::new(stderr).lines(); while let Ok(Some(line)) = reader.next_line().await { yield Ok(classify_line(&line)); } } let code = child.wait().await.map(|s| s.code().unwrap_or(0)).unwrap_or(1); yield Ok(Event::default().event("done").data(code.to_string())); } } } } // ── Helpers ─────────────────────────────────────────────────────────────────── fn el_binary() -> String { if let Ok(path) = which::which("el") { return path.to_string_lossy().to_string(); } "el".to_string() }