1use 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#[derive(Debug, Subcommand)]
25pub enum TemplateCommand {
26 List {
28 #[arg(long)]
30 remote: bool,
31 },
32
33 Fetch {
35 repo: String,
37 #[arg(short, long)]
39 reference: Option<String>,
40 #[arg(short, long)]
42 force: bool,
43 },
44
45 Install {
47 name: String,
49 #[arg(short, long)]
51 as_name: Option<String>,
52 },
53
54 Update {
56 template: Option<String>,
58 #[arg(long)]
60 check: bool,
61 },
62
63 Create {
65 name: String,
67 #[arg(short, long)]
69 output: Option<PathBuf>,
70 #[arg(short, long, default_value = ".")]
72 project: PathBuf,
73 },
74
75 Remove {
77 name: String,
79 #[arg(short, long)]
81 yes: bool,
82 },
83
84 Info {
86 template: String,
88 #[arg(long)]
90 cache: bool,
91 },
92
93 Validate {
95 path: PathBuf,
97 },
98}
99
100impl TemplateCommand {
101 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
142async fn list_templates(remote: bool) -> Result<()> {
144 println!("{}", style("📚 Available Templates").cyan().bold());
145 println!();
146
147 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 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 let total = builtin.len() + cached_count;
184 println!("Total: {} template(s) available\n", total);
185
186 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
206async fn fetch_template(repo: &str, reference: Option<&str>, force: bool) -> Result<()> {
208 println!("{}", style("📦 Fetching Template").cyan().bold());
209 println!();
210
211 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 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 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
272async 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 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 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 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
334async 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 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 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
425async 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 if !project.exists() {
443 return Err(Error::template(format!(
444 "Project path does not exist: {}",
445 project.display()
446 )));
447 }
448
449 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 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 println!("Creating template at: {}", output_dir.display());
465
466 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
490async 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
529async 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 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}