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    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
103/// Show detailed information about a workflow.
104pub 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
174/// Delete a workflow file.
175pub 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
183/// Discover all `.zug` files in a base directory and its `workflows/` subdirectory.
184fn 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
194/// Collect `.zug` files from a single directory into `out`.
195fn 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;