use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use super::def::WorkflowDef;
use super::parser::parse_workflow_from_file;
#[derive(Debug, Clone)]
pub struct WorkflowInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
pub path: PathBuf,
pub source: WorkflowSource,
pub tags: Vec<String>,
pub required_inputs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WorkflowSource {
Project,
User,
}
pub struct WorkflowRegistry {
workflows: HashMap<String, WorkflowInfo>,
project_path: Option<PathBuf>,
user_path: PathBuf,
}
impl WorkflowRegistry {
pub fn new(project_dir: Option<&PathBuf>) -> Self {
let user_path = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".matrix")
.join("workflows");
let project_path = project_dir.map(|p| p.join(".matrix").join("workflows"));
let mut registry = Self {
workflows: HashMap::new(),
project_path,
user_path,
};
if let Err(e) = registry.discover() {
log::warn!("Workflow discovery failed: {}", e);
}
registry
}
pub fn new_global() -> Self {
Self::new(None)
}
pub fn discover(&mut self) -> Result<()> {
self.workflows.clear();
let project_path = self.project_path.clone();
let user_path = self.user_path.clone();
if let Some(proj) = project_path
&& proj.exists()
{
self.discover_from_dir(&proj, WorkflowSource::Project)?;
}
if user_path.exists() {
self.discover_from_dir(&user_path, WorkflowSource::User)?;
}
log::info!("Discovered {} workflows", self.workflows.len());
Ok(())
}
fn discover_from_dir(&mut self, dir: &PathBuf, source: WorkflowSource) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path
.extension()
.is_some_and(|ext| ext == "yaml" || ext == "yml")
&& let Ok(workflow) = parse_workflow_from_file(&path)
{
let info = WorkflowInfo {
id: workflow.id.clone(),
name: workflow.name.clone(),
description: workflow.description.clone(),
path: path.clone(),
source: source.clone(),
tags: self.extract_tags(&workflow),
required_inputs: workflow
.inputs
.iter()
.filter(|i| i.required)
.map(|i| i.name.clone())
.collect(),
};
self.workflows.insert(workflow.id.clone(), info);
}
}
Ok(())
}
fn extract_tags(&self, workflow: &WorkflowDef) -> Vec<String> {
let mut tags = Vec::new();
tags.push(workflow.id.clone());
tags.extend(workflow.name.split_whitespace().map(|s| s.to_lowercase()));
for node in &workflow.nodes {
let type_tag = match node.node_type {
super::def::NodeType::Task => "task",
super::def::NodeType::Condition => "condition",
super::def::NodeType::Parallel => "parallel",
super::def::NodeType::Approval => "approval",
super::def::NodeType::Wait => "wait",
super::def::NodeType::SubWorkflow => "subworkflow",
super::def::NodeType::Start => "start",
super::def::NodeType::End => "end",
};
tags.push(type_tag.to_string());
if let Some(ref task) = node.task {
tags.push(task.clone());
}
}
tags
}
pub fn list(&self) -> Vec<&WorkflowInfo> {
self.workflows.values().collect()
}
pub fn get(&self, id: &str) -> Option<&WorkflowInfo> {
self.workflows.get(id)
}
pub fn match_workflows(&self, query: &str) -> Vec<&WorkflowInfo> {
let query_lower = query.to_lowercase();
let query_words: Vec<&str> = query_lower.split_whitespace().collect();
let mut scored: Vec<(usize, &WorkflowInfo)> = self
.workflows
.values()
.map(|info| {
let score = self.calculate_match_score(info, &query_words, &query_lower);
(score, info)
})
.filter(|(score, _)| *score > 0)
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0));
scored.iter().map(|(_, info)| *info).collect()
}
fn calculate_match_score(
&self,
info: &WorkflowInfo,
query_words: &[&str],
query_lower: &str,
) -> usize {
let mut score = 0;
if info.id.to_lowercase() == query_lower {
score += 100;
}
if info.name.to_lowercase().contains(query_lower) {
score += 50;
}
for tag in &info.tags {
let tag_lower = tag.to_lowercase();
for word in query_words {
if tag_lower == *word {
score += 30;
}
if tag_lower.contains(word) {
score += 10;
}
}
if query_lower.contains(&tag_lower) {
score += 15;
}
}
if let Some(ref desc) = info.description {
let desc_lower = desc.to_lowercase();
for word in query_words {
if desc_lower.contains(word) {
score += 5;
}
}
}
score
}
pub fn load_workflow(&self, id: &str) -> Result<Option<WorkflowDef>> {
if let Some(info) = self.get(id) {
let workflow = parse_workflow_from_file(&info.path)?;
Ok(Some(workflow))
} else {
Ok(None)
}
}
pub fn best_match(&self, query: &str) -> Option<&WorkflowInfo> {
self.match_workflows(query).first().copied()
}
pub fn is_empty(&self) -> bool {
self.workflows.is_empty()
}
pub fn count(&self) -> usize {
self.workflows.len()
}
pub fn generate_summary(&self) -> String {
if self.is_empty() {
return "No workflows available.".to_string();
}
let mut summary = format!("Available workflows ({}):\n\n", self.count());
for info in self.list() {
let source = if info.source == WorkflowSource::Project {
"project"
} else {
"global"
};
summary.push_str(&format!("• {} - {} [{}]\n", info.id, info.name, source));
if let Some(ref desc) = info.description {
let desc_short = desc.chars().take(50).collect::<String>();
summary.push_str(&format!(" {}\n", desc_short));
}
if !info.required_inputs.is_empty() {
summary.push_str(&format!(
" Required inputs: {}\n",
info.required_inputs.join(", ")
));
}
}
summary.push_str("\nUsage: 'run workflow <id> with <inputs>' or describe your intent.");
summary
}
}
impl Default for WorkflowRegistry {
fn default() -> Self {
Self::new_global()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_creation() {
let registry = WorkflowRegistry::new_global();
let _count = registry.count();
}
#[test]
fn test_match_empty_registry() {
let registry = WorkflowRegistry::new_global();
let matches = registry.match_workflows("test query");
assert!(matches.is_empty() || registry.count() > 0);
}
#[test]
fn test_generate_summary() {
let registry = WorkflowRegistry::new_global();
let summary = registry.generate_summary();
assert!(summary.contains("workflows") || summary.contains("No workflows"));
}
#[test]
fn test_discover_image_article_workflow() {
let registry = WorkflowRegistry::new_global();
let info = registry.get("image-article");
if let Some(workflow_info) = info {
assert_eq!(workflow_info.id, "image-article");
assert_eq!(workflow_info.name, "Image Article Generator");
assert!(workflow_info.required_inputs.contains(&"topic".to_string()));
} else {
assert!(
registry.get("hello-world").is_some(),
"Neither image-article nor hello-world workflows found in ~/.matrix/workflows/"
);
}
}
}