use axum::extract::Query;
use axum::Json;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::lfd::http::ApiResult;
#[derive(Deserialize)]
pub struct ListFlowsQuery {
repo: Option<String>,
}
#[derive(Debug, Serialize)]
struct StepSummary {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default_agent: Option<String>,
}
#[derive(Debug, Serialize)]
struct FlowSummary {
name: String,
steps: Vec<String>,
}
#[derive(Debug, Serialize)]
struct FlowsResult {
flows: Vec<FlowSummary>,
steps: Vec<StepSummary>,
directions: Vec<String>,
supported_harnesses: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct FlowsResponse {
ok: bool,
result: FlowsResult,
}
pub async fn list_flows_handler(Query(query): Query<ListFlowsQuery>) -> ApiResult<FlowsResponse> {
let repo = query.repo.unwrap_or_default();
let repo_path = PathBuf::from(repo);
let flows = list_flows(&repo_path);
let steps = list_steps(&repo_path);
let directions = crate::lf::discovery::list_directions(Some(&repo_path));
let supported_harnesses =
crate::engine::config::load_config_or_default(Some(&repo_path)).supported_harnesses;
Ok(Json(FlowsResponse {
ok: true,
result: FlowsResult {
flows,
steps,
directions,
supported_harnesses,
},
}))
}
fn list_flows(repo: &Path) -> Vec<FlowSummary> {
let mut flows: HashMap<String, Vec<String>> = HashMap::new();
for (name, steps) in list_repo_flows(repo) {
flows.insert(name, steps);
}
for name in crate::engine::builtins::builtin_flow_names() {
let name = name.to_string();
if flows.contains_key(&name) {
continue;
}
if let Some(steps) = load_flow_steps(&name, repo) {
flows.insert(name, steps);
}
}
let mut result: Vec<FlowSummary> = flows
.into_iter()
.map(|(name, steps)| FlowSummary { name, steps })
.collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
fn list_repo_flows(repo: &Path) -> Vec<(String, Vec<String>)> {
let mut result = Vec::new();
let flows_dir = repo.join(".lf/flows");
let Ok(entries) = std::fs::read_dir(flows_dir) else {
return result;
};
for entry in entries.flatten() {
let path = entry.path();
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
continue;
};
if !matches!(ext, "yaml" | "yml" | "json") {
continue;
}
let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if let Some(steps) = load_flow_steps(name, repo) {
result.push((name.to_string(), steps));
}
}
result
}
pub(super) fn load_flow_steps(name: &str, repo: &Path) -> Option<Vec<String>> {
let flow = crate::engine::flow::load_flow(name, repo).ok()?;
let items = crate::engine::flow::expand_flow(&flow, repo).ok()?;
Some(extract_step_names(&items))
}
fn extract_step_names(items: &[crate::engine::flow::ConcreteItem]) -> Vec<String> {
let mut names = Vec::new();
for item in items {
match item {
crate::engine::flow::ConcreteItem::Step(step) => {
names.push(step.step.name.clone());
}
crate::engine::flow::ConcreteItem::Op(ops) => {
names.push(ops.item.to_string());
}
crate::engine::flow::ConcreteItem::And(and) => {
for branch in &and.branches {
for step in &branch.steps {
names.push(step.step.name.clone());
}
}
}
crate::engine::flow::ConcreteItem::Xor(_) => {
names.push("[xor]".to_string());
}
crate::engine::flow::ConcreteItem::Or(_) => {
names.push("[or]".to_string());
}
crate::engine::flow::ConcreteItem::Loop(_) => {
names.push("[loop]".to_string());
}
}
}
names
}
fn list_steps(repo: &Path) -> Vec<StepSummary> {
let mut names: HashSet<String> = crate::engine::builtins::builtin_step_names()
.into_iter()
.map(|name| name.to_string())
.collect();
let steps_dir = repo.join(".lf/steps");
if let Ok(entries) = std::fs::read_dir(steps_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
names.insert(name.to_string());
}
}
}
let mut list: Vec<StepSummary> = names
.into_iter()
.map(|name| load_step_summary(repo, name))
.collect();
list.sort_by(|a, b| a.name.cmp(&b.name));
list
}
fn load_step_summary(repo: &Path, name: String) -> StepSummary {
match crate::engine::flow::load_step(&name, repo) {
Ok(step) => StepSummary {
name,
agent: step.agent,
default_agent: step.default_agent,
},
Err(_) => StepSummary {
name,
agent: None,
default_agent: None,
},
}
}