Skip to main content

ferrous_forge/commands/template/
mod.rs

1//! Template management commands
2//!
3//! @task T021
4//! @epic T014
5
6use crate::templates::TemplateRegistry;
7use crate::templates::repository::TemplateRepository;
8use crate::templates::repository::github::GitHubClient;
9use crate::templates::validation::validate_before_install;
10use crate::{Error, Result};
11use clap::Subcommand;
12use console::style;
13use std::path::PathBuf;
14
15mod creation;
16mod display;
17mod utils;
18
19pub use creation::*;
20pub use display::*;
21pub use utils::*;
22
23/// Template subcommands
24#[derive(Debug, Subcommand)]
25pub enum TemplateCommand {
26    /// List available templates (local + cached)
27    List {
28        /// Show remote templates too
29        #[arg(long)]
30        remote: bool,
31    },
32
33    /// Fetch template from GitHub repository
34    Fetch {
35        /// GitHub repository reference (e.g., gh:user/repo or user/repo)
36        repo: String,
37        /// Branch, tag, or commit to fetch
38        #[arg(short, long)]
39        reference: Option<String>,
40        /// Force re-fetch even if already cached
41        #[arg(short, long)]
42        force: bool,
43    },
44
45    /// Install a cached template locally
46    Install {
47        /// Template name (from cache) or repository
48        name: String,
49        /// Custom name for the installed template
50        #[arg(short, long)]
51        as_name: Option<String>,
52    },
53
54    /// Update all installed templates or a specific one
55    Update {
56        /// Specific template to update
57        template: Option<String>,
58        /// Check for updates without installing
59        #[arg(long)]
60        check: bool,
61    },
62
63    /// Create a new template from current project
64    Create {
65        /// Name for the new template
66        name: String,
67        /// Output directory for template
68        #[arg(short, long)]
69        output: Option<PathBuf>,
70        /// Project directory to template-ize
71        #[arg(short, long, default_value = ".")]
72        project: PathBuf,
73    },
74
75    /// Remove a cached template
76    Remove {
77        /// Template name to remove
78        name: String,
79        /// Skip confirmation
80        #[arg(short, long)]
81        yes: bool,
82    },
83
84    /// Show detailed information about a template
85    Info {
86        /// Template name
87        template: String,
88        /// Show cache information
89        #[arg(long)]
90        cache: bool,
91    },
92
93    /// Validate a template manifest
94    Validate {
95        /// Path to template directory or manifest
96        path: PathBuf,
97    },
98}
99
100impl TemplateCommand {
101    /// Execute the template command
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the template cannot be found, variables are
106    /// invalid, or the project files cannot be written.
107    pub async fn execute(&self) -> Result<()> {
108        match self {
109            TemplateCommand::List { remote } => list_templates(*remote).await,
110
111            TemplateCommand::Fetch {
112                repo,
113                reference,
114                force,
115            } => fetch_template(repo, reference.as_deref(), *force).await,
116
117            TemplateCommand::Install { name, as_name } => {
118                install_template(name, as_name.as_deref()).await
119            }
120
121            TemplateCommand::Update { template, check } => {
122                update_templates(template.as_deref(), *check).await
123            }
124
125            TemplateCommand::Create {
126                name,
127                output,
128                project,
129            } => create_template_from_project(name, output.as_deref(), project).await,
130
131            TemplateCommand::Remove { name, yes } => remove_template(name, *yes).await,
132
133            TemplateCommand::Info { template, cache } => {
134                show_template_info_extended(template, *cache).await
135            }
136
137            TemplateCommand::Validate { path } => validate_template_manifest(path).await,
138        }
139    }
140}
141
142/// List available templates
143async fn list_templates(remote: bool) -> Result<()> {
144    println!("{}", style("📚 Available Templates").cyan().bold());
145    println!();
146
147    // List built-in templates
148    let registry = TemplateRegistry::new();
149    let builtin = registry.list_templates();
150
151    if !builtin.is_empty() {
152        println!("{}", style("Built-in Templates:").white().bold());
153        for (name, _kind, description) in &builtin {
154            println!("  {} {}", style("•").cyan(), style(name).white().bold());
155            println!("    {}", style(description).dim());
156        }
157        println!();
158    }
159
160    // List cached templates
161    let repo = TemplateRepository::new()?;
162    let cached_count = repo.list_cached().len();
163
164    if cached_count > 0 {
165        println!("{}", style("Cached Templates:").white().bold());
166        for template in repo.list_cached() {
167            println!(
168                "  {} {} {}",
169                style("•").cyan(),
170                style(&template.name).white().bold(),
171                style(format!("(v{})", template.version)).dim()
172            );
173            println!("    Source: {}", style(&template.source).dim());
174            println!(
175                "    Updated: {}",
176                style(template.updated_at.format("%Y-%m-%d %H:%M")).dim()
177            );
178        }
179        println!();
180    }
181
182    // Show count
183    let total = builtin.len() + cached_count;
184    println!("Total: {} template(s) available\n", total);
185
186    // Show usage
187    println!(
188        "Use {} to fetch a template from GitHub",
189        style("ferrous-forge template fetch <repo>").cyan()
190    );
191    println!(
192        "Use {} to create a project from a template",
193        style("ferrous-forge template install <name>").cyan()
194    );
195
196    if remote {
197        println!(
198            "\n{}",
199            style("Remote template discovery not yet implemented").yellow()
200        );
201    }
202
203    Ok(())
204}
205
206/// Fetch template from GitHub
207async fn fetch_template(repo: &str, reference: Option<&str>, force: bool) -> Result<()> {
208    println!("{}", style("📦 Fetching Template").cyan().bold());
209    println!();
210
211    // Parse repo reference
212    let mut repo_ref = GitHubClient::parse_repo_ref(repo)?;
213    if let Some(git_ref) = reference {
214        repo_ref.git_ref = Some(git_ref.to_string());
215    }
216
217    println!("Repository: {}/{}", repo_ref.owner, repo_ref.repo);
218    if let Some(git_ref) = &repo_ref.git_ref {
219        println!("Reference: {}", git_ref);
220    }
221    println!();
222
223    // Check if already cached
224    let mut repository = TemplateRepository::new()?;
225    let cache_name = format!("{}-{}", repo_ref.owner, repo_ref.repo);
226
227    if !force && repository.is_cached(&cache_name) {
228        let cached = repository.get_cached(&cache_name).ok_or_else(|| {
229            Error::validation(format!(
230                "Failed to retrieve cached template '{}'",
231                cache_name
232            ))
233        })?;
234        println!(
235            "{}",
236            style(format!("Template '{}' already cached", cache_name)).yellow()
237        );
238        println!("Use --force to re-fetch");
239        println!();
240        println!("Cached version: {}", cached.version);
241        println!(
242            "Last updated: {}",
243            cached.updated_at.format("%Y-%m-%d %H:%M")
244        );
245        return Ok(());
246    }
247
248    // Fetch from GitHub
249    let client = GitHubClient::new()?;
250    println!("{}", style("Fetching template...").dim());
251
252    let template = client.fetch_template(&repo_ref, &mut repository).await?;
253
254    println!();
255    println!(
256        "{}",
257        style("✅ Template fetched successfully!").green().bold()
258    );
259    println!();
260    println!("Name: {}", style(&template.name).cyan());
261    println!("Version: {}", style(&template.version).cyan());
262    println!("Description: {}", template.manifest.description);
263    println!();
264    println!(
265        "Use {} to install this template",
266        style(format!("ferrous-forge template install {}", template.name)).cyan()
267    );
268
269    Ok(())
270}
271
272/// Install a cached template
273async fn install_template(name: &str, _as_name: Option<&str>) -> Result<()> {
274    println!("{}", style("📥 Installing Template").cyan().bold());
275    println!();
276
277    let repository = TemplateRepository::new()?;
278
279    // Check if it's a cached template
280    if let Some(template) = repository.get_cached(name) {
281        println!("Template: {}", style(name).cyan());
282        println!("Source: {}", template.source);
283        println!("Version: {}", template.version);
284        println!();
285
286        // Validate before installing
287        let validation = validate_before_install(&template.cache_path).await?;
288
289        if !validation.valid {
290            println!("{}", style("❌ Template validation failed:").red().bold());
291            for error in &validation.errors {
292                println!("  • {}", error);
293            }
294            return Err(Error::template(
295                "Template validation failed - cannot install",
296            ));
297        }
298
299        if !validation.warnings.is_empty() {
300            println!("{}", style("⚠️  Warnings:").yellow());
301            for warning in &validation.warnings {
302                println!("  • {}", warning);
303            }
304            println!();
305        }
306
307        println!(
308            "{}",
309            style("✅ Template validated and ready to use!")
310                .green()
311                .bold()
312        );
313        println!();
314        println!(
315            "Use {} to create a project from this template",
316            style(format!(
317                "ferrous-forge template create {} <output-dir>",
318                name
319            ))
320            .cyan()
321        );
322
323        Ok(())
324    } else {
325        // Try to fetch it first
326        println!("Template '{}' not found in cache.", name);
327        println!("Attempting to fetch from GitHub...");
328        println!();
329
330        fetch_template(name, None, false).await
331    }
332}
333
334/// Update templates
335async fn update_templates(template: Option<&str>, check: bool) -> Result<()> {
336    if check {
337        println!("{}", style("🔍 Checking for Updates").cyan().bold());
338    } else {
339        println!("{}", style("🔄 Updating Templates").cyan().bold());
340    }
341    println!();
342
343    let mut repository = TemplateRepository::new()?;
344
345    if let Some(name) = template {
346        // Update specific template
347        if let Some(template) = repository.get_cached(name).cloned() {
348            if check {
349                println!("Template: {}", name);
350                println!("Current version: {}", template.version);
351                println!(
352                    "Last update: {}",
353                    template.updated_at.format("%Y-%m-%d %H:%M")
354                );
355                if template.needs_update() {
356                    println!("{}", style("Update available!").green());
357                } else {
358                    println!("{}", style("Up to date").green());
359                }
360            } else {
361                let client = GitHubClient::new()?;
362                let repo_ref = GitHubClient::parse_repo_ref(&template.source)?;
363                client.fetch_template(&repo_ref, &mut repository).await?;
364                println!(
365                    "{}",
366                    style(format!("✅ Updated template '{}'", name)).green()
367                );
368            }
369        } else {
370            return Err(Error::template(format!(
371                "Template '{}' not found in cache",
372                name
373            )));
374        }
375    } else {
376        // Update all templates
377        let templates_to_update: Vec<_> = repository
378            .list_cached()
379            .iter()
380            .map(|t| (t.name.clone(), t.source.clone(), t.needs_update()))
381            .collect();
382
383        if templates_to_update.is_empty() {
384            println!("No cached templates to update.");
385            return Ok(());
386        }
387
388        let client = GitHubClient::new()?;
389        let mut updated = 0;
390
391        for (name, source, needs_update) in templates_to_update {
392            if check {
393                print!("{}: ", name);
394                if needs_update {
395                    println!("{}", style("update available").yellow());
396                } else {
397                    println!("{}", style("up to date").green());
398                }
399            } else {
400                let repo_ref = GitHubClient::parse_repo_ref(&source)?;
401                match client.fetch_template(&repo_ref, &mut repository).await {
402                    Ok(_) => {
403                        println!("{}", style(format!("✅ Updated {}", name)).green());
404                        updated += 1;
405                    }
406                    Err(e) => {
407                        println!(
408                            "{}",
409                            style(format!("❌ Failed to update {}: {}", name, e)).red()
410                        );
411                    }
412                }
413            }
414        }
415
416        if !check {
417            println!();
418            println!("Updated {} template(s)", updated);
419        }
420    }
421
422    Ok(())
423}
424
425/// Create a template from current project
426async fn create_template_from_project(
427    name: &str,
428    output: Option<&std::path::Path>,
429    project: &std::path::Path,
430) -> Result<()> {
431    println!(
432        "{}",
433        style("📋 Creating Template from Project").cyan().bold()
434    );
435    println!();
436
437    println!("Template name: {}", style(name).cyan());
438    println!("Project path: {}", project.display());
439    println!();
440
441    // Validate project exists
442    if !project.exists() {
443        return Err(Error::template(format!(
444            "Project path does not exist: {}",
445            project.display()
446        )));
447    }
448
449    // Check for Cargo.toml
450    let cargo_toml = project.join("Cargo.toml");
451    if !cargo_toml.exists() {
452        return Err(Error::template(
453            "No Cargo.toml found - is this a Rust project?",
454        ));
455    }
456
457    // Determine output directory
458    let output_dir = output.map_or_else(
459        || std::env::current_dir().map(|d| d.join(name)),
460        |o| Ok(o.join(name)),
461    )?;
462
463    // Create template structure
464    println!("Creating template at: {}", output_dir.display());
465
466    // TODO: Implement template generation from project
467    // This would:
468    // 1. Copy project files
469    // 2. Create template.toml manifest
470    // 3. Replace project-specific values with template variables
471    // 4. Add .templateignore support
472
473    println!();
474    println!("{}", style("✅ Template created!").green().bold());
475    println!();
476    println!("Next steps:");
477    println!(
478        "  1. Edit {} to configure variables",
479        output_dir.join("template.toml").display()
480    );
481    println!(
482        "  2. Test with: ferrous-forge template validate {}",
483        output_dir.display()
484    );
485    println!("  3. Publish to GitHub and share with: ferrous-forge template fetch gh:user/repo");
486
487    Ok(())
488}
489
490/// Remove a cached template
491async fn remove_template(name: &str, yes: bool) -> Result<()> {
492    let mut repository = TemplateRepository::new()?;
493
494    if !repository.is_cached(name) {
495        return Err(Error::template(format!(
496            "Template '{}' not found in cache",
497            name
498        )));
499    }
500
501    if !yes {
502        println!("{}", style("⚠️  Remove Template").yellow().bold());
503        println!();
504        println!("This will remove '{}' from the cache.", name);
505        println!();
506
507        let confirm = dialoguer::Confirm::new()
508            .with_prompt("Are you sure?")
509            .default(false)
510            .interact()
511            .map_err(|e| Error::template(format!("Failed to get confirmation: {e}")))?;
512
513        if !confirm {
514            println!("Cancelled.");
515            return Ok(());
516        }
517    }
518
519    repository.remove_from_cache(name)?;
520
521    println!(
522        "{}",
523        style(format!("✅ Removed template '{}'", name)).green()
524    );
525
526    Ok(())
527}
528
529/// Show extended template information
530async fn show_template_info_extended(template: &str, show_cache: bool) -> Result<()> {
531    let repository = TemplateRepository::new()?;
532
533    if let Some(cached) = repository.get_cached(template) {
534        println!("{}", style("📋 Template Information").cyan().bold());
535        println!();
536        println!("Name: {}", style(&cached.name).white().bold());
537        println!("Source: {}", cached.source);
538        println!("Version: {}", cached.version);
539        println!("Description: {}", cached.manifest.description);
540        println!("Author: {}", cached.manifest.author);
541        println!("Kind: {:?}", cached.manifest.kind);
542        println!();
543
544        if show_cache {
545            println!("{}", style("Cache Information:").white().bold());
546            println!("  Cache path: {}", cached.cache_path.display());
547            println!(
548                "  Fetched: {}",
549                cached.fetched_at.format("%Y-%m-%d %H:%M:%S")
550            );
551            println!(
552                "  Updated: {}",
553                cached.updated_at.format("%Y-%m-%d %H:%M:%S")
554            );
555            println!();
556        }
557
558        if !cached.manifest.variables.is_empty() {
559            println!("{}", style("Variables:").white().bold());
560            for var in &cached.manifest.variables {
561                let req = if var.required { "required" } else { "optional" };
562                println!(
563                    "  • {} ({}) - {}",
564                    style(&var.name).cyan(),
565                    req,
566                    var.description
567                );
568            }
569        }
570    } else {
571        // Try built-in
572        let registry = TemplateRegistry::new();
573        if registry.get_builtin(template).is_some() {
574            show_template_info(template).await?;
575        } else {
576            return Err(Error::template(format!(
577                "Template '{}' not found",
578                template
579            )));
580        }
581    }
582
583    Ok(())
584}