use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use clap::{Args, Subcommand};
use ironflow_templates::fetch::fetch_repo;
use ironflow_templates::install::install_template;
use ironflow_templates::registry::{discover_templates, find_template};
#[derive(Debug, Args)]
pub struct TemplateArgs {
#[command(subcommand)]
pub command: TemplateCommands,
}
#[derive(Debug, Subcommand)]
pub enum TemplateCommands {
List {
source: String,
},
Info {
source: String,
name: String,
},
Add {
source: String,
name: String,
#[arg(long, short)]
output: Option<PathBuf>,
},
Create {
name: String,
#[arg(long, short)]
output: Option<PathBuf>,
},
}
pub fn execute(args: &TemplateArgs) -> Result<()> {
match &args.command {
TemplateCommands::List { source } => cmd_list(source),
TemplateCommands::Info { source, name } => cmd_info(source, name),
TemplateCommands::Add {
source,
name,
output,
} => cmd_add(source, name, output.as_deref()),
TemplateCommands::Create { name, output } => cmd_create(name, output.as_deref()),
}
}
fn resolve_source(source: &str) -> Result<ResolvedSource> {
if source.contains("://") {
let tmp = fetch_repo(source)?;
Ok(ResolvedSource::Cloned(tmp))
} else {
let path = PathBuf::from(source);
if !path.exists() {
anyhow::bail!("path does not exist: {source}");
}
Ok(ResolvedSource::Local(path))
}
}
enum ResolvedSource {
Local(PathBuf),
Cloned(tempfile::TempDir),
}
impl ResolvedSource {
fn path(&self) -> &Path {
match self {
ResolvedSource::Local(p) => p,
ResolvedSource::Cloned(tmp) => tmp.path(),
}
}
}
fn cmd_list(source: &str) -> Result<()> {
let resolved = resolve_source(source)?;
let templates = discover_templates(resolved.path())?;
if templates.is_empty() {
println!("No templates found in {source}");
return Ok(());
}
println!("Available templates:");
println!();
for (name, (manifest, _dir)) in &templates {
println!(" {name} (v{})", manifest.template.version);
println!(" {}", manifest.template.description);
if let Some(cat) = &manifest.template.category {
println!(" category: {cat}");
}
println!();
}
Ok(())
}
fn cmd_info(source: &str, name: &str) -> Result<()> {
let resolved = resolve_source(source)?;
let (manifest, _dir) = find_template(resolved.path(), name)?;
println!("Template: {}", manifest.template.name);
println!("Version: {}", manifest.template.version);
println!("Description: {}", manifest.template.description);
if !manifest.template.authors.is_empty() {
println!("Authors: {}", manifest.template.authors.join(", "));
}
if let Some(license) = &manifest.template.license {
println!("License: {license}");
}
if let Some(cat) = &manifest.template.category {
println!("Category: {cat}");
}
if !manifest.dependencies.is_empty() {
println!();
println!("Dependencies:");
let mut deps: Vec<_> = manifest.dependencies.keys().collect();
deps.sort();
for dep in deps {
println!(" - {dep}");
}
}
Ok(())
}
fn git_user_name() -> String {
Command::new("git")
.args(["config", "user.name"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "Author".to_string())
}
fn cmd_create(name: &str, output: Option<&Path>) -> Result<()> {
let dest = output
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(name));
if dest.exists() {
anyhow::bail!("directory '{}' already exists", dest.display());
}
let src_dir = dest.join("src");
fs::create_dir_all(&src_dir)?;
let author = git_user_name();
let template_toml = format!(
r#"[template]
name = "{name}"
version = "0.1.0"
description = "A workflow template"
authors = ["{author}"]
[dependencies]
"#
);
fs::write(dest.join("template.toml"), template_toml)?;
let handler = format!(
r#"use ironflow_engine::context::WorkflowContext;
use ironflow_engine::handler::{{HandlerFuture, WorkflowHandler}};
pub struct {handler_name};
impl WorkflowHandler for {handler_name} {{
fn name(&self) -> &str {{
"{name}"
}}
fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {{
Box::pin(async move {{
ctx.shell("hello", "echo 'Hello from {name}!'").await?;
Ok(())
}})
}}
}}
"#,
handler_name = to_pascal_case(name)
);
fs::write(src_dir.join("handler.rs"), handler)?;
println!("Template '{name}' created at {}", dest.display());
Ok(())
}
fn to_pascal_case(s: &str) -> String {
s.split(['-', '_'])
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
}
})
.collect()
}
fn cmd_add(source: &str, name: &str, output: Option<&Path>) -> Result<()> {
let resolved = resolve_source(source)?;
let (manifest, template_dir) = find_template(resolved.path(), name)?;
let destination = match output {
Some(path) => path.to_path_buf(),
None => PathBuf::from(format!("src/workflows/{name}")),
};
let result = install_template(&manifest, &template_dir, &destination)?;
println!(
"Template '{name}' installed to {}",
result.destination.display()
);
if !result.dependencies.is_empty() {
println!();
println!("Add these dependencies to your Cargo.toml:");
println!();
for dep in &result.dependencies {
println!(" {dep}");
}
}
Ok(())
}