Skip to main content

zig_core/
manage.rs

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/// Summary information about a discovered workflow file.
11#[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
19/// Return structured data about all discovered workflows.
20pub 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
57/// Return the parsed workflow for a given workflow name or path.
58pub fn get_workflow_detail(workflow: &str) -> Result<Workflow, ZigError> {
59    let path = resolve_workflow_path(workflow)?;
60    parser::parse_file(&path)
61}
62
63/// List all `.zug` workflow files found in the current directory, `./workflows/`,
64/// and the global `~/.zig/workflows/` directory.
65pub 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    // Determine terminal width, default to 100 if unavailable.
75    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    // Reserve space for name, steps, separators, and a minimum path column,
86    // then give the rest to description.
87    let fixed = name_w + steps_w + 8; // 3 x 2-char gaps + 2 for padding
88    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
118/// Show detailed information about a workflow.
119pub 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
189/// Delete a workflow file.
190pub 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
198/// Truncate a string to `max` characters, appending "…" if truncated.
199fn 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
209/// Format step count concisely.
210fn format_steps(count: usize) -> String {
211    if count == 1 {
212        "1 step".to_string()
213    } else {
214        format!("{count} steps")
215    }
216}
217
218/// Try to detect terminal width from the COLUMNS environment variable.
219fn terminal_width() -> Option<usize> {
220    std::env::var("COLUMNS").ok().and_then(|v| v.parse().ok())
221}
222
223/// Discover all `.zug` files in a base directory and its `workflows/` subdirectory.
224fn 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
234/// Collect `.zug` files from a single directory into `out`.
235fn 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;