ironflow_cli/commands/
template.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7use anyhow::Result;
8use clap::{Args, Subcommand};
9
10use ironflow_templates::fetch::fetch_repo;
11use ironflow_templates::install::install_template;
12use ironflow_templates::registry::{discover_templates, find_template};
13
14#[derive(Debug, Args)]
16pub struct TemplateArgs {
17 #[command(subcommand)]
19 pub command: TemplateCommands,
20}
21
22#[derive(Debug, Subcommand)]
24pub enum TemplateCommands {
25 List {
27 source: String,
29 },
30 Info {
32 source: String,
34 name: String,
36 },
37 Add {
39 source: String,
41 name: String,
43 #[arg(long, short)]
45 output: Option<PathBuf>,
46 },
47 Create {
49 name: String,
51 #[arg(long, short)]
53 output: Option<PathBuf>,
54 },
55}
56
57pub fn execute(args: &TemplateArgs) -> Result<()> {
63 match &args.command {
64 TemplateCommands::List { source } => cmd_list(source),
65 TemplateCommands::Info { source, name } => cmd_info(source, name),
66 TemplateCommands::Add {
67 source,
68 name,
69 output,
70 } => cmd_add(source, name, output.as_deref()),
71 TemplateCommands::Create { name, output } => cmd_create(name, output.as_deref()),
72 }
73}
74
75fn resolve_source(source: &str) -> Result<ResolvedSource> {
81 if source.contains("://") {
82 let tmp = fetch_repo(source)?;
83 Ok(ResolvedSource::Cloned(tmp))
84 } else {
85 let path = PathBuf::from(source);
86 if !path.exists() {
87 anyhow::bail!("path does not exist: {source}");
88 }
89 Ok(ResolvedSource::Local(path))
90 }
91}
92
93enum ResolvedSource {
94 Local(PathBuf),
95 Cloned(tempfile::TempDir),
96}
97
98impl ResolvedSource {
99 fn path(&self) -> &Path {
100 match self {
101 ResolvedSource::Local(p) => p,
102 ResolvedSource::Cloned(tmp) => tmp.path(),
103 }
104 }
105}
106
107fn cmd_list(source: &str) -> Result<()> {
108 let resolved = resolve_source(source)?;
109 let templates = discover_templates(resolved.path())?;
110
111 if templates.is_empty() {
112 println!("No templates found in {source}");
113 return Ok(());
114 }
115
116 println!("Available templates:");
117 println!();
118 for (name, (manifest, _dir)) in &templates {
119 println!(" {name} (v{})", manifest.template.version);
120 println!(" {}", manifest.template.description);
121 if let Some(cat) = &manifest.template.category {
122 println!(" category: {cat}");
123 }
124 println!();
125 }
126
127 Ok(())
128}
129
130fn cmd_info(source: &str, name: &str) -> Result<()> {
131 let resolved = resolve_source(source)?;
132 let (manifest, _dir) = find_template(resolved.path(), name)?;
133
134 println!("Template: {}", manifest.template.name);
135 println!("Version: {}", manifest.template.version);
136 println!("Description: {}", manifest.template.description);
137
138 if !manifest.template.authors.is_empty() {
139 println!("Authors: {}", manifest.template.authors.join(", "));
140 }
141 if let Some(license) = &manifest.template.license {
142 println!("License: {license}");
143 }
144 if let Some(cat) = &manifest.template.category {
145 println!("Category: {cat}");
146 }
147
148 if !manifest.dependencies.is_empty() {
149 println!();
150 println!("Dependencies:");
151 let mut deps: Vec<_> = manifest.dependencies.keys().collect();
152 deps.sort();
153 for dep in deps {
154 println!(" - {dep}");
155 }
156 }
157
158 Ok(())
159}
160
161fn git_user_name() -> String {
162 Command::new("git")
163 .args(["config", "user.name"])
164 .output()
165 .ok()
166 .filter(|o| o.status.success())
167 .and_then(|o| String::from_utf8(o.stdout).ok())
168 .map(|s| s.trim().to_string())
169 .filter(|s| !s.is_empty())
170 .unwrap_or_else(|| "Author".to_string())
171}
172
173fn cmd_create(name: &str, output: Option<&Path>) -> Result<()> {
174 let dest = output
175 .map(PathBuf::from)
176 .unwrap_or_else(|| PathBuf::from(name));
177
178 if dest.exists() {
179 anyhow::bail!("directory '{}' already exists", dest.display());
180 }
181
182 let src_dir = dest.join("src");
183 fs::create_dir_all(&src_dir)?;
184
185 let author = git_user_name();
186 let template_toml = format!(
187 r#"[template]
188name = "{name}"
189version = "0.1.0"
190description = "A workflow template"
191authors = ["{author}"]
192
193[dependencies]
194"#
195 );
196 fs::write(dest.join("template.toml"), template_toml)?;
197
198 let handler = format!(
199 r#"use ironflow_engine::context::WorkflowContext;
200use ironflow_engine::handler::{{HandlerFuture, WorkflowHandler}};
201
202pub struct {handler_name};
203
204impl WorkflowHandler for {handler_name} {{
205 fn name(&self) -> &str {{
206 "{name}"
207 }}
208
209 fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {{
210 Box::pin(async move {{
211 ctx.shell("hello", "echo 'Hello from {name}!'").await?;
212 Ok(())
213 }})
214 }}
215}}
216"#,
217 handler_name = to_pascal_case(name)
218 );
219 fs::write(src_dir.join("handler.rs"), handler)?;
220
221 println!("Template '{name}' created at {}", dest.display());
222
223 Ok(())
224}
225
226fn to_pascal_case(s: &str) -> String {
227 s.split(['-', '_'])
228 .filter(|part| !part.is_empty())
229 .map(|part| {
230 let mut chars = part.chars();
231 match chars.next() {
232 None => String::new(),
233 Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
234 }
235 })
236 .collect()
237}
238
239fn cmd_add(source: &str, name: &str, output: Option<&Path>) -> Result<()> {
240 let resolved = resolve_source(source)?;
241 let (manifest, template_dir) = find_template(resolved.path(), name)?;
242
243 let destination = match output {
244 Some(path) => path.to_path_buf(),
245 None => PathBuf::from(format!("src/workflows/{name}")),
246 };
247
248 let result = install_template(&manifest, &template_dir, &destination)?;
249
250 println!(
251 "Template '{name}' installed to {}",
252 result.destination.display()
253 );
254
255 if !result.dependencies.is_empty() {
256 println!();
257 println!("Add these dependencies to your Cargo.toml:");
258 println!();
259 for dep in &result.dependencies {
260 println!(" {dep}");
261 }
262 }
263
264 Ok(())
265}