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 term_width = terminal_width().unwrap_or(100);
76
77 let name_w = infos.iter().map(|r| r.name.len()).max().unwrap_or(0).max(4);
78 let steps_w = infos
79 .iter()
80 .map(|r| format_steps(r.step_count).len())
81 .max()
82 .unwrap_or(0)
83 .max(5);
84
85 let fixed = name_w + steps_w + 8; let desc_w = if term_width > fixed + 20 {
89 term_width - fixed - 20
90 } else {
91 30
92 };
93 let desc_w = desc_w.max(11);
94
95 println!(
96 "\x1b[1m{:<name_w$}\x1b[0m {:<desc_w$} {:<steps_w$} PATH",
97 "NAME", "DESCRIPTION", "STEPS"
98 );
99 println!(
100 "{} {} {} {}",
101 "─".repeat(name_w),
102 "─".repeat(desc_w),
103 "─".repeat(steps_w),
104 "─".repeat(4)
105 );
106 for info in &infos {
107 let desc = truncate(&info.description, desc_w);
108 let steps = format_steps(info.step_count);
109 println!(
110 "\x1b[1m{:<name_w$}\x1b[0m {:<desc_w$} {:<steps_w$} {}",
111 info.name, desc, steps, info.path
112 );
113 }
114
115 Ok(())
116}
117
118pub fn show_workflow(workflow: &str) -> Result<(), ZigError> {
120 let path = resolve_workflow_path(workflow)?;
121 let wf = parser::parse_file(&path)?;
122
123 println!("Name: {}", wf.workflow.name);
124 println!("Path: {}", path.display());
125 if !wf.workflow.description.is_empty() {
126 println!("Description: {}", wf.workflow.description);
127 }
128 if !wf.workflow.tags.is_empty() {
129 println!("Tags: {}", wf.workflow.tags.join(", "));
130 }
131 if let Some(ref version) = wf.workflow.version {
132 println!("Version: {version}");
133 }
134 if let Some(ref provider) = wf.workflow.provider {
135 print!("Provider: {provider}");
136 if let Some(ref model) = wf.workflow.model {
137 print!(" / {model}");
138 }
139 println!();
140 } else if let Some(ref model) = wf.workflow.model {
141 println!("Model: {model}");
142 }
143
144 if !wf.vars.is_empty() {
145 println!("\nVariables:");
146 let mut vars: Vec<_> = wf.vars.iter().collect();
147 vars.sort_by_key(|(name, _)| (*name).clone());
148 for (name, var) in &vars {
149 let default = match &var.default {
150 Some(v) => format!(" = {v}"),
151 None => String::new(),
152 };
153 println!(" {name}: {}{default}", var.var_type);
154 if !var.description.is_empty() {
155 println!(" {}", var.description);
156 }
157 }
158 }
159
160 if !wf.steps.is_empty() {
161 println!("\nSteps ({}):", wf.steps.len());
162 for (i, step) in wf.steps.iter().enumerate() {
163 print!(" {}. {}", i + 1, step.name);
164 if !step.depends_on.is_empty() {
165 print!(" (depends on: {})", step.depends_on.join(", "));
166 }
167 println!();
168 if !step.description.is_empty() {
169 println!(" {}", step.description);
170 }
171 if let Some(condition) = &step.condition {
172 println!(" condition: {condition}");
173 }
174 if let Some(provider) = &step.provider {
175 print!(" provider: {provider}");
176 if let Some(model) = &step.model {
177 print!(" / {model}");
178 }
179 println!();
180 } else if let Some(model) = &step.model {
181 println!(" model: {model}");
182 }
183 }
184 }
185
186 Ok(())
187}
188
189pub fn delete_workflow(workflow: &str) -> Result<(), ZigError> {
191 let path = resolve_workflow_path(workflow)?;
192 std::fs::remove_file(&path)
193 .map_err(|e| ZigError::Io(format!("failed to delete {}: {e}", path.display())))?;
194 println!("deleted {}", path.display());
195 Ok(())
196}
197
198fn truncate(s: &str, max: usize) -> String {
200 if s.len() <= max {
201 s.to_string()
202 } else if max <= 1 {
203 "…".to_string()
204 } else {
205 format!("{}…", &s[..max - 1])
206 }
207}
208
209fn format_steps(count: usize) -> String {
211 if count == 1 {
212 "1 step".to_string()
213 } else {
214 format!("{count} steps")
215 }
216}
217
218fn terminal_width() -> Option<usize> {
220 std::env::var("COLUMNS").ok().and_then(|v| v.parse().ok())
221}
222
223fn discover_zug_files(base: &Path) -> Vec<PathBuf> {
225 let mut files = Vec::new();
226
227 collect_zug_files(base, &mut files);
228 collect_zug_files(&base.join("workflows"), &mut files);
229
230 files.sort();
231 files
232}
233
234fn collect_zug_files(dir: &Path, out: &mut Vec<PathBuf>) {
236 if let Ok(entries) = std::fs::read_dir(dir) {
237 for entry in entries.flatten() {
238 let path = entry.path();
239 if path.extension().is_some_and(|ext| ext == "zug") && path.is_file() {
240 out.push(path);
241 }
242 }
243 }
244}
245
246#[cfg(test)]
247#[path = "manage_tests.rs"]
248mod tests;