Skip to main content

ironflow_cli/commands/
template.rs

1//! `ironflow-cli template` subcommands.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::Result;
6use clap::{Args, Subcommand};
7
8use ironflow_templates::fetch::fetch_repo;
9use ironflow_templates::install::install_template;
10use ironflow_templates::registry::{discover_templates, find_template};
11
12/// Manage workflow templates.
13#[derive(Debug, Args)]
14pub struct TemplateArgs {
15    /// Subcommand to execute.
16    #[command(subcommand)]
17    pub command: TemplateCommands,
18}
19
20/// Template subcommands.
21#[derive(Debug, Subcommand)]
22pub enum TemplateCommands {
23    /// List available templates in a repository (local path or remote Git URL).
24    List {
25        /// Local path or Git URL containing templates.
26        source: String,
27    },
28    /// Show details about a specific template.
29    Info {
30        /// Local path or Git URL containing templates.
31        source: String,
32        /// Template name.
33        name: String,
34    },
35    /// Add a template from a repository into your project.
36    Add {
37        /// Local path or Git URL containing templates.
38        source: String,
39        /// Template name to install.
40        name: String,
41        /// Output directory (default: `src/workflows/<name>`).
42        #[arg(long, short)]
43        output: Option<PathBuf>,
44    },
45}
46
47/// Execute a template subcommand.
48///
49/// # Errors
50///
51/// Returns an error if fetching, parsing, or installing fails.
52pub fn execute(args: &TemplateArgs) -> Result<()> {
53    match &args.command {
54        TemplateCommands::List { source } => cmd_list(source),
55        TemplateCommands::Info { source, name } => cmd_info(source, name),
56        TemplateCommands::Add {
57            source,
58            name,
59            output,
60        } => cmd_add(source, name, output.as_deref()),
61    }
62}
63
64/// Resolve a source string to a local path.
65///
66/// If the source looks like a URL (contains `://`), clone it into a
67/// temporary directory and return the path. Otherwise treat it as a
68/// local path.
69fn resolve_source(source: &str) -> Result<ResolvedSource> {
70    if source.contains("://") {
71        let tmp = fetch_repo(source)?;
72        Ok(ResolvedSource::Cloned(tmp))
73    } else {
74        let path = PathBuf::from(source);
75        if !path.exists() {
76            anyhow::bail!("path does not exist: {source}");
77        }
78        Ok(ResolvedSource::Local(path))
79    }
80}
81
82enum ResolvedSource {
83    Local(PathBuf),
84    Cloned(tempfile::TempDir),
85}
86
87impl ResolvedSource {
88    fn path(&self) -> &Path {
89        match self {
90            ResolvedSource::Local(p) => p,
91            ResolvedSource::Cloned(tmp) => tmp.path(),
92        }
93    }
94}
95
96fn cmd_list(source: &str) -> Result<()> {
97    let resolved = resolve_source(source)?;
98    let templates = discover_templates(resolved.path())?;
99
100    if templates.is_empty() {
101        println!("No templates found in {source}");
102        return Ok(());
103    }
104
105    println!("Available templates:");
106    println!();
107    for (name, (manifest, _dir)) in &templates {
108        println!("  {name} (v{})", manifest.template.version);
109        println!("    {}", manifest.template.description);
110        if let Some(cat) = &manifest.template.category {
111            println!("    category: {cat}");
112        }
113        println!();
114    }
115
116    Ok(())
117}
118
119fn cmd_info(source: &str, name: &str) -> Result<()> {
120    let resolved = resolve_source(source)?;
121    let (manifest, _dir) = find_template(resolved.path(), name)?;
122
123    println!("Template: {}", manifest.template.name);
124    println!("Version:  {}", manifest.template.version);
125    println!("Description: {}", manifest.template.description);
126
127    if !manifest.template.authors.is_empty() {
128        println!("Authors: {}", manifest.template.authors.join(", "));
129    }
130    if let Some(license) = &manifest.template.license {
131        println!("License: {license}");
132    }
133    if let Some(cat) = &manifest.template.category {
134        println!("Category: {cat}");
135    }
136
137    if !manifest.dependencies.is_empty() {
138        println!();
139        println!("Dependencies:");
140        let mut deps: Vec<_> = manifest.dependencies.keys().collect();
141        deps.sort();
142        for dep in deps {
143            println!("  - {dep}");
144        }
145    }
146
147    Ok(())
148}
149
150fn cmd_add(source: &str, name: &str, output: Option<&Path>) -> Result<()> {
151    let resolved = resolve_source(source)?;
152    let (manifest, template_dir) = find_template(resolved.path(), name)?;
153
154    let destination = match output {
155        Some(path) => path.to_path_buf(),
156        None => PathBuf::from(format!("src/workflows/{name}")),
157    };
158
159    let result = install_template(&manifest, &template_dir, &destination)?;
160
161    println!(
162        "Template '{name}' installed to {}",
163        result.destination.display()
164    );
165
166    if !result.dependencies.is_empty() {
167        println!();
168        println!("Add these dependencies to your Cargo.toml:");
169        println!();
170        for dep in &result.dependencies {
171            println!("  {dep}");
172        }
173    }
174
175    Ok(())
176}