1use 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::registry::{discover_templates, find_template};
12
13mod registry_ops;
14
15#[derive(Debug, Args)]
17pub struct TemplateArgs {
18 #[command(subcommand)]
20 pub command: TemplateCommands,
21}
22
23#[derive(Debug, Subcommand)]
25pub enum TemplateCommands {
26 List {
28 source: Option<String>,
30 #[arg(long)]
32 registry: bool,
33 #[arg(long)]
35 registry_url: Option<String>,
36 },
37 Info {
39 source: String,
41 name: String,
43 },
44 Add {
46 name: String,
48 #[arg(long)]
50 from: Option<String>,
51 #[arg(long)]
53 registry: bool,
54 #[arg(long)]
56 registry_url: Option<String>,
57 #[arg(long, short)]
61 output: Option<PathBuf>,
62 #[arg(long)]
64 force: bool,
65 },
66 Create {
68 name: String,
70 #[arg(long, short)]
72 output: Option<PathBuf>,
73 },
74 Update {
76 name: Option<String>,
78 #[arg(long)]
80 check: bool,
81 #[arg(long)]
83 force: bool,
84 #[arg(long)]
86 registry_url: Option<String>,
87 },
88}
89
90pub fn execute(args: &TemplateArgs) -> Result<()> {
96 match &args.command {
97 TemplateCommands::List {
98 source,
99 registry,
100 registry_url,
101 } => {
102 if *registry || source.is_none() {
103 registry_ops::cmd_list_registry(registry_url.as_deref())
104 } else {
105 cmd_list(source.as_deref().unwrap())
106 }
107 }
108 TemplateCommands::Info { source, name } => cmd_info(source, name),
109 TemplateCommands::Add {
110 name,
111 from,
112 registry_url,
113 output,
114 force,
115 ..
116 } => registry_ops::cmd_add(
117 name,
118 from.as_deref(),
119 registry_url.as_deref(),
120 output.as_deref(),
121 *force,
122 ),
123 TemplateCommands::Create { name, output } => cmd_create(name, output.as_deref()),
124 TemplateCommands::Update {
125 name,
126 check,
127 force,
128 registry_url,
129 } => registry_ops::cmd_update(name.as_deref(), *check, *force, registry_url.as_deref()),
130 }
131}
132
133pub(crate) fn resolve_source(source: &str) -> Result<ResolvedSource> {
135 if source.contains("://") {
136 let tmp = fetch_repo(source)?;
137 Ok(ResolvedSource::Cloned(tmp))
138 } else {
139 let path = PathBuf::from(source);
140 if !path.exists() {
141 anyhow::bail!("path does not exist: {source}");
142 }
143 Ok(ResolvedSource::Local(path))
144 }
145}
146
147pub(crate) enum ResolvedSource {
148 Local(PathBuf),
149 Cloned(tempfile::TempDir),
150}
151
152impl ResolvedSource {
153 pub(crate) fn path(&self) -> &Path {
154 match self {
155 ResolvedSource::Local(p) => p,
156 ResolvedSource::Cloned(tmp) => tmp.path(),
157 }
158 }
159}
160
161pub(crate) fn parse_name_version(input: &str) -> (&str, Option<&str>) {
163 match input.split_once('@') {
164 Some((name, version)) => (name, Some(version)),
165 None => (input, None),
166 }
167}
168
169pub(crate) fn to_pascal_case(s: &str) -> String {
171 s.split(['-', '_'])
172 .filter(|part| !part.is_empty())
173 .map(|part| {
174 let mut chars = part.chars();
175 match chars.next() {
176 None => String::new(),
177 Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
178 }
179 })
180 .collect()
181}
182
183fn cmd_list(source: &str) -> Result<()> {
184 let resolved = resolve_source(source)?;
185 let templates = discover_templates(resolved.path())?;
186
187 if templates.is_empty() {
188 println!("No templates found in {source}");
189 return Ok(());
190 }
191
192 println!("Available templates:");
193 println!();
194 for (name, (manifest, _dir)) in &templates {
195 println!(" {name} (v{})", manifest.template.version);
196 println!(" {}", manifest.template.description);
197 if let Some(cat) = &manifest.template.category {
198 println!(" category: {cat}");
199 }
200 println!();
201 }
202
203 Ok(())
204}
205
206fn cmd_info(source: &str, name: &str) -> Result<()> {
207 let resolved = resolve_source(source)?;
208 let (manifest, _dir) = find_template(resolved.path(), name)?;
209
210 println!("Template: {}", manifest.template.name);
211 println!("Version: {}", manifest.template.version);
212 println!("Description: {}", manifest.template.description);
213
214 if !manifest.template.authors.is_empty() {
215 println!("Authors: {}", manifest.template.authors.join(", "));
216 }
217 if let Some(license) = &manifest.template.license {
218 println!("License: {license}");
219 }
220 if let Some(cat) = &manifest.template.category {
221 println!("Category: {cat}");
222 }
223 if let Some(min_ver) = &manifest.template.min_ironflow_version {
224 println!("Requires Ironflow: >= {min_ver}");
225 }
226
227 if !manifest.dependencies.is_empty() {
228 println!();
229 println!("Dependencies:");
230 let mut deps: Vec<_> = manifest.dependencies.keys().collect();
231 deps.sort();
232 for dep in deps {
233 println!(" - {dep}");
234 }
235 }
236
237 let requirements = manifest.requirements.render();
238 if !requirements.is_empty() {
239 println!();
240 print!("{requirements}");
241 }
242
243 Ok(())
244}
245
246fn git_user_name() -> String {
247 Command::new("git")
248 .args(["config", "user.name"])
249 .output()
250 .ok()
251 .filter(|o| o.status.success())
252 .and_then(|o| String::from_utf8(o.stdout).ok())
253 .map(|s| s.trim().to_string())
254 .filter(|s| !s.is_empty())
255 .unwrap_or_else(|| "Author".to_string())
256}
257
258fn cmd_create(name: &str, output: Option<&Path>) -> Result<()> {
259 let dest = output
260 .map(PathBuf::from)
261 .unwrap_or_else(|| PathBuf::from(name));
262
263 if dest.exists() {
264 anyhow::bail!("directory '{}' already exists", dest.display());
265 }
266
267 let src_dir = dest.join("src");
268 fs::create_dir_all(&src_dir)?;
269
270 let author = git_user_name();
271 let template_toml = format!(
272 r#"[template]
273name = "{name}"
274version = "0.1.0"
275description = "A workflow template"
276authors = ["{author}"]
277
278[dependencies]
279"#
280 );
281 fs::write(dest.join("template.toml"), template_toml)?;
282
283 let handler = format!(
284 r#"use ironflow_engine::context::WorkflowContext;
285use ironflow_engine::handler::{{HandlerFuture, WorkflowHandler}};
286
287pub struct {handler_name};
288
289impl WorkflowHandler for {handler_name} {{
290 fn name(&self) -> &str {{
291 "{name}"
292 }}
293
294 fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {{
295 Box::pin(async move {{
296 ctx.shell("hello", "echo 'Hello from {name}!'").await?;
297 Ok(())
298 }})
299 }}
300}}
301"#,
302 handler_name = to_pascal_case(name)
303 );
304 fs::write(src_dir.join("handler.rs"), handler)?;
305
306 println!("Template '{name}' created at {}", dest.display());
307
308 Ok(())
309}