Archived
feat: el-ide — native IDE for engram-lang, LSP, type graph, plugin ecosystem
Axum HTTP server (port 7771) serving a single-page IDE with CodeMirror 6 syntax highlighting for engram-lang, a force-directed type graph visualizer, LSP (completions, hover, diagnostics), SSE-streamed build/run output, a plugin host with five first-party plugins, and a reasoning panel that proxies to engram-server. 28 tests across three crates, zero warnings.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
//! 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<String>,
|
||||
}
|
||||
|
||||
#[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<AppState>,
|
||||
Json(req): Json<BuildRequest>,
|
||||
) -> 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<AppState>,
|
||||
Json(req): Json<RunRequest>,
|
||||
) -> 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<Item = Result<Event, Infallible>> {
|
||||
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<Item = Result<Event, Infallible>> {
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user