use crate::orchestrate::deployer;
use crate::orchestrate::http_inject::HttpNodeEntry;
use crate::ui::routes::wizard::{download_url, find_file_in_dir, zip_directory};
use crate::ui::state::{AppState, LogKind, PackLogLine, WizardJobStatus, WizardMode};
use std::path::Path;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
#[allow(clippy::too_many_arguments)]
pub async fn run_wizard_pipeline(
state: &Arc<AppState>,
job_id: &str,
args: &[String],
out_dir: &Path,
pack_name: &str,
providers: &[String],
mode: &WizardMode,
cloud: Option<&str>,
http_entries: &[HttpNodeEntry],
) {
push_log(
state,
job_id,
"Running greentic-cards2pack...",
LogKind::Progress,
)
.await;
set_step(state, job_id, "Building cards...", 10).await;
let mut cmd = Command::new("greentic-cards2pack");
cmd.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
set_failed(
state,
job_id,
&format!("Failed to spawn greentic-cards2pack: {e}"),
)
.await;
return;
}
};
if let Some(stderr) = child.stderr.take() {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(raw)) = reader.next_line().await {
for sub in raw.split('\r').map(|s| s.trim()).filter(|s| !s.is_empty()) {
let line = sub.to_string();
let (kind, pct) = classify_line(&line);
let step = summarize_step(&line);
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
if line.contains("Progress:")
&& let Some(pos) = job.lines.iter().rposition(|l| {
l.text.contains("Progress:")
&& l.text.split("Progress:").next()
== line.split("Progress:").next()
})
{
job.lines.remove(pos);
}
job.lines.push(PackLogLine { text: line, kind });
if let Some(p) = pct {
job.progress = p;
}
if !step.is_empty() {
job.step = step;
}
}
}
}
}
let success = child.wait().await.is_ok_and(|s| s.success());
if !success {
set_failed(state, job_id, "greentic-cards2pack failed").await;
return;
}
let dist = out_dir.join("dist");
let pack_path = find_file_in_dir(&dist, "gtpack");
let Some(pack_path) = pack_path else {
set_failed(state, job_id, "No .gtpack file found in dist/").await;
return;
};
if !http_entries.is_empty() {
let flow_path = out_dir.join("flows").join("main.ygtc");
if let Ok(ygtc) = std::fs::read_to_string(&flow_path) {
let injected = crate::orchestrate::http_inject::inject_http_nodes(&ygtc, http_entries);
let _ = std::fs::write(&flow_path, injected);
let pack_yaml = out_dir.join("pack.yaml");
let _ = crate::orchestrate::http_inject::ensure_component_http_source(&pack_yaml);
}
push_log(
state,
job_id,
&format!("Injected {} HTTP API node(s) into flow", http_entries.len()),
LogKind::Done,
)
.await;
}
set_step(state, job_id, "Building .gtbundle...", 70).await;
push_log(state, job_id, "Building .gtbundle...", LogKind::Progress).await;
let bundle_result = tokio::task::spawn_blocking({
let pack_path = pack_path.clone();
let pack_name = pack_name.to_string();
let providers = providers.to_vec();
move || deployer::build_bundle(&pack_path, &pack_name, Some(&providers))
})
.await;
let bundle_result = match bundle_result {
Ok(Ok(br)) => br,
Ok(Err(e)) => {
set_failed(state, job_id, &format!("Bundle build failed: {e}")).await;
return;
}
Err(e) => {
set_failed(state, job_id, &format!("Bundle build task panicked: {e}")).await;
return;
}
};
match mode {
WizardMode::Deploy => {
let cloud_name = cloud.unwrap_or("aws");
set_step(state, job_id, "Deploying to cloud...", 85).await;
{
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.status = WizardJobStatus::Deploying;
}
}
let deploy_result = tokio::task::spawn_blocking({
let bp = bundle_result.bundle_path.clone();
let name = pack_name.to_string();
let cloud_str = cloud_name.to_string();
move || deployer::deploy_bundle(&bp, &name, &cloud_str)
})
.await;
match deploy_result {
Ok(Ok(dr)) => {
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.status = WizardJobStatus::Done;
job.progress = 100;
job.step = "Deployed!".to_string();
job.deploy_url = Some(dr.deploy_url.clone());
job.lines.push(PackLogLine {
text: format!("Deployed: {}", dr.deploy_url),
kind: LogKind::Done,
});
}
}
Ok(Err(e)) => {
set_failed(state, job_id, &format!("Deploy failed: {e}")).await;
}
Err(e) => {
set_failed(state, job_id, &format!("Deploy task panicked: {e}")).await;
}
}
}
WizardMode::Develop => {
set_step(state, job_id, "Zipping bundle workspace...", 88).await;
let zip_name = format!("{pack_name}-bundle.zip");
let zip_path = std::env::temp_dir().join(&zip_name);
let src = bundle_result.workspace_path.clone();
let dst = zip_path.clone();
let zip_err = tokio::task::spawn_blocking(move || zip_directory(&src, &dst)).await;
match zip_err {
Ok(Ok(())) => {
let dl = download_url(&zip_path);
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.status = WizardJobStatus::Done;
job.progress = 100;
job.step = "Ready to download!".to_string();
job.download_url = Some(dl.clone());
job.filename = Some(zip_name.clone());
job.lines.push(PackLogLine {
text: format!("Bundle workspace zipped: {zip_name}"),
kind: LogKind::Done,
});
}
}
Ok(Err(e)) => {
let fname = bundle_result
.bundle_path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{pack_name}.gtbundle"));
let dl = download_url(&bundle_result.bundle_path);
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.status = WizardJobStatus::Done;
job.progress = 100;
job.step = "Ready (zip failed, bundle available)".to_string();
job.download_url = Some(dl);
job.filename = Some(fname);
job.lines.push(PackLogLine {
text: format!("Zip failed ({e}), serving .gtbundle instead"),
kind: LogKind::Warning,
});
}
}
Err(e) => {
set_failed(state, job_id, &format!("Zip task panicked: {e}")).await;
}
}
}
}
}
async fn set_failed(state: &Arc<AppState>, job_id: &str, msg: &str) {
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.status = WizardJobStatus::Failed;
job.error = Some(msg.to_string());
job.lines.push(PackLogLine {
text: msg.to_string(),
kind: LogKind::Error,
});
}
}
async fn push_log(state: &Arc<AppState>, job_id: &str, msg: &str, kind: LogKind) {
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.lines.push(PackLogLine {
text: msg.to_string(),
kind,
});
}
}
async fn set_step(state: &Arc<AppState>, job_id: &str, step: &str, progress: u8) {
let mut jobs = state.wizard_jobs.lock().await;
if let Some(job) = jobs.get_mut(job_id) {
job.step = step.to_string();
job.progress = progress;
}
}
pub fn classify_line(line: &str) -> (LogKind, Option<u8>) {
if line.contains("Progress:") {
let pct = line.split("Progress:").nth(1).and_then(|s| {
let parts: Vec<&str> = s.trim().split('/').collect();
if parts.len() == 2 {
let cur: f32 = parts[0].trim().parse().ok()?;
let total: f32 = parts[1].trim().parse().ok()?;
Some((15.0 + (cur / total) * 45.0) as u8)
} else {
None
}
});
(LogKind::Progress, pct)
} else if line.contains("error") || line.contains("Error") || line.starts_with("ERR") {
(LogKind::Error, None)
} else if line.contains("warning") || line.contains("Warning") {
(LogKind::Warning, None)
} else if line.contains("wrote") || line.contains("Pack:") || line.contains("OK ") {
(LogKind::Done, Some(65))
} else if line.contains("[flow]") {
(LogKind::Progress, Some(25))
} else if line.contains("pack.yaml") {
(LogKind::Progress, Some(45))
} else if line.contains("Running:") {
(LogKind::Progress, Some(60))
} else {
(LogKind::Info, None)
}
}
pub fn summarize_step(line: &str) -> String {
if line.contains("created pack at") {
"Creating workspace...".to_string()
} else if line.contains("[flow]") {
line.trim().to_string()
} else if line.contains("pack.yaml updated") {
"Updating pack manifest...".to_string()
} else if line.starts_with("OK ") || line.contains("valid") {
"Validating flows...".to_string()
} else if line.contains("wrote") && line.contains(".gtpack") {
"Writing .gtpack archive...".to_string()
} else if line.contains("Running:") {
"Running greentic-pack build...".to_string()
} else if line.contains("Pack:") && !line.contains("pack.yaml") {
"Pack summary...".to_string()
} else {
String::new()
}
}