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