1use std::path::{Path, PathBuf};
2
3use serde::Serialize;
4
5use crate::error::ZigError;
6use crate::run::resolve_workflow_path;
7use crate::workflow::model::Workflow;
8use crate::workflow::parser;
9
10#[derive(Debug, Clone, Serialize)]
12pub struct WorkflowInfo {
13 pub name: String,
14 pub description: String,
15 pub step_count: usize,
16 pub path: String,
17}
18
19pub fn get_workflow_list() -> Result<Vec<WorkflowInfo>, ZigError> {
21 let mut entries = discover_zug_files(Path::new("."));
22
23 if let Some(global_dir) = crate::paths::global_workflows_dir() {
24 for f in discover_zug_files(&global_dir) {
25 if !entries.iter().any(|e| e.file_name() == f.file_name()) {
26 entries.push(f);
27 }
28 }
29 }
30
31 let mut infos = Vec::new();
32 for path in &entries {
33 let display = path.display().to_string();
34 match parser::parse_file(path) {
35 Ok(wf) => {
36 infos.push(WorkflowInfo {
37 name: wf.workflow.name,
38 description: wf.workflow.description,
39 step_count: wf.steps.len(),
40 path: display,
41 });
42 }
43 Err(_) => {
44 infos.push(WorkflowInfo {
45 name: "(parse error)".to_string(),
46 description: String::new(),
47 step_count: 0,
48 path: display,
49 });
50 }
51 }
52 }
53
54 Ok(infos)
55}
56
57pub fn get_workflow_detail(workflow: &str) -> Result<Workflow, ZigError> {
59 let path = resolve_workflow_path(workflow)?;
60 parser::parse_file(&path)
61}
62
63pub fn list_workflows() -> Result<(), ZigError> {
66 let infos = get_workflow_list()?;
67
68 if infos.is_empty() {
69 println!("No workflows found.");
70 println!("Hint: create one with `zig workflow create <name>`");
71 return Ok(());
72 }
73
74 let name_w = infos.iter().map(|r| r.name.len()).max().unwrap_or(0).max(4);
75 let desc_w = infos
76 .iter()
77 .map(|r| r.description.len())
78 .max()
79 .unwrap_or(0)
80 .max(11);
81 let steps_w = infos
82 .iter()
83 .map(|r| format!("{} steps", r.step_count).len())
84 .max()
85 .unwrap_or(0)
86 .max(5);
87
88 println!(
89 "{:<name_w$} {:<desc_w$} {:<steps_w$} PATH",
90 "NAME", "DESCRIPTION", "STEPS"
91 );
92 for info in &infos {
93 let steps = format!("{} steps", info.step_count);
94 println!(
95 "{:<name_w$} {:<desc_w$} {:<steps_w$} {}",
96 info.name, info.description, steps, info.path
97 );
98 }
99
100 Ok(())
101}
102
103pub fn show_workflow(workflow: &str) -> Result<(), ZigError> {
105 let path = resolve_workflow_path(workflow)?;
106 let wf = parser::parse_file(&path)?;
107
108 println!("Name: {}", wf.workflow.name);
109 println!("Path: {}", path.display());
110 if !wf.workflow.description.is_empty() {
111 println!("Description: {}", wf.workflow.description);
112 }
113 if !wf.workflow.tags.is_empty() {
114 println!("Tags: {}", wf.workflow.tags.join(", "));
115 }
116 if let Some(ref version) = wf.workflow.version {
117 println!("Version: {version}");
118 }
119 if let Some(ref provider) = wf.workflow.provider {
120 print!("Provider: {provider}");
121 if let Some(ref model) = wf.workflow.model {
122 print!(" / {model}");
123 }
124 println!();
125 } else if let Some(ref model) = wf.workflow.model {
126 println!("Model: {model}");
127 }
128
129 if !wf.vars.is_empty() {
130 println!("\nVariables:");
131 let mut vars: Vec<_> = wf.vars.iter().collect();
132 vars.sort_by_key(|(name, _)| (*name).clone());
133 for (name, var) in &vars {
134 let default = match &var.default {
135 Some(v) => format!(" = {v}"),
136 None => String::new(),
137 };
138 println!(" {name}: {}{default}", var.var_type);
139 if !var.description.is_empty() {
140 println!(" {}", var.description);
141 }
142 }
143 }
144
145 if !wf.steps.is_empty() {
146 println!("\nSteps ({}):", wf.steps.len());
147 for (i, step) in wf.steps.iter().enumerate() {
148 print!(" {}. {}", i + 1, step.name);
149 if !step.depends_on.is_empty() {
150 print!(" (depends on: {})", step.depends_on.join(", "));
151 }
152 println!();
153 if !step.description.is_empty() {
154 println!(" {}", step.description);
155 }
156 if let Some(condition) = &step.condition {
157 println!(" condition: {condition}");
158 }
159 if let Some(provider) = &step.provider {
160 print!(" provider: {provider}");
161 if let Some(model) = &step.model {
162 print!(" / {model}");
163 }
164 println!();
165 } else if let Some(model) = &step.model {
166 println!(" model: {model}");
167 }
168 }
169 }
170
171 Ok(())
172}
173
174pub fn delete_workflow(workflow: &str) -> Result<(), ZigError> {
176 let path = resolve_workflow_path(workflow)?;
177 std::fs::remove_file(&path)
178 .map_err(|e| ZigError::Io(format!("failed to delete {}: {e}", path.display())))?;
179 println!("deleted {}", path.display());
180 Ok(())
181}
182
183fn discover_zug_files(base: &Path) -> Vec<PathBuf> {
185 let mut files = Vec::new();
186
187 collect_zug_files(base, &mut files);
188 collect_zug_files(&base.join("workflows"), &mut files);
189
190 files.sort();
191 files
192}
193
194fn collect_zug_files(dir: &Path, out: &mut Vec<PathBuf>) {
196 if let Ok(entries) = std::fs::read_dir(dir) {
197 for entry in entries.flatten() {
198 let path = entry.path();
199 if path.extension().is_some_and(|ext| ext == "zug") && path.is_file() {
200 out.push(path);
201 }
202 }
203 }
204}
205
206#[cfg(test)]
207#[path = "manage_tests.rs"]
208mod tests;