anesis 0.9.1

CLI for scaffolding projects from remote templates and extending them with project addons
Documentation
use std::{fs, path::Path};

use anyhow::Result;
use chrono::Utc;
use comfy_table::{Attribute, Cell, Table};
use serde::{Deserialize, Serialize};

use crate::{AppContext, templates::AnesisTemplate};

#[derive(Serialize, Deserialize)]
pub struct TemplatesCache {
  #[serde(rename = "lastUpdated")]
  pub last_updated: String,
  pub templates: Vec<CachedTemplate>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CachedTemplate {
  pub name: String,
  pub version: String,
  pub source: String,
  pub path: String,
  pub commit_sha: String,
}

pub fn update_templates_cache(
  template_path: &Path,
  path: &Path,
  commit_sha: &str,
) -> Result<CachedTemplate> {
  let anesis_json = template_path.join(path).join("anesis.template.json");
  let content = fs::read_to_string(&anesis_json)?;
  let template_info: AnesisTemplate = serde_json::from_str(&content)?;

  let templates_json = template_path.join("anesis-templates.json");
  let mut templates_info: TemplatesCache = if templates_json.exists() {
    let content = fs::read_to_string(&templates_json)?;
    serde_json::from_str(&content)?
  } else {
    TemplatesCache {
      last_updated: Utc::now().to_rfc3339(),
      templates: Vec::new(),
    }
  };

  templates_info.last_updated = Utc::now().to_rfc3339();

  // Replace existing entry to avoid duplicates on re-download
  templates_info
    .templates
    .retain(|t| t.name != template_info.name);
  let cached_template = CachedTemplate {
    name: template_info.name,
    version: template_info.version,
    source: template_info.repository.url,
    path: path.to_string_lossy().to_string(),
    commit_sha: commit_sha.to_string(),
  };
  templates_info.templates.push(cached_template.clone());

  fs::write(
    &templates_json,
    serde_json::to_string_pretty(&templates_info)?,
  )?;

  Ok(cached_template)
}

pub fn get_cached_template(ctx: &AppContext, name: &str) -> Result<Option<CachedTemplate>> {
  let templates_json = ctx.paths.templates.join("anesis-templates.json");

  if !templates_json.exists() {
    return Ok(None);
  }

  let content = fs::read_to_string(&templates_json)?;
  let templates_info: TemplatesCache = serde_json::from_str(&content)?;

  Ok(
    templates_info
      .templates
      .into_iter()
      .find(|t| t.name == name),
  )
}

pub fn remove_template_from_cache(template_path: &Path, template_name: &str) -> Result<()> {
  let templates_json = template_path.join("anesis-templates.json");

  if !templates_json.exists() {
    return Err(anyhow::anyhow!(
      "Template '{}' is not installed",
      template_name
    ));
  }

  let content = fs::read_to_string(&templates_json)?;
  let mut templates_info: TemplatesCache = serde_json::from_str(&content)?;

  let exists = templates_info
    .templates
    .iter()
    .any(|t| t.name == template_name);
  if !exists {
    return Err(anyhow::anyhow!(
      "Template '{}' is not installed",
      template_name
    ));
  }

  templates_info.last_updated = Utc::now().to_rfc3339();

  if let Some(t) = templates_info
    .templates
    .iter()
    .find(|t| t.name == template_name)
  {
    let cleanup_path = template_path.join(&t.path);

    if cleanup_path.exists() {
      if let Err(e) = fs::remove_dir_all(&cleanup_path) {
        println!("Failed to remove: {}", e);
      }

      let mut current = cleanup_path.parent();
      while let Some(parent) = current {
        if parent == template_path {
          break;
        }
        if fs::remove_dir(parent).is_err() {
          break;
        }
        current = parent.parent();
      }
    }
  }

  templates_info
    .templates
    .retain(|template| template.name != template_name);

  fs::write(
    &templates_json,
    serde_json::to_string_pretty(&templates_info)?,
  )?;

  println!("✓ Removed template '{}'", template_name);
  Ok(())
}

pub fn get_installed_templates(template_path: &Path) -> Result<()> {
  let templates_json = template_path.join("anesis-templates.json");

  let templates_info: TemplatesCache = if templates_json.exists() {
    let content = fs::read_to_string(&templates_json)?;
    serde_json::from_str(&content)?
  } else {
    TemplatesCache {
      last_updated: Utc::now().to_rfc3339(),
      templates: Vec::new(),
    }
  };

  if templates_info.templates.is_empty() {
    println!("No templates installed yet.");
    return Ok(());
  }

  let mut table = Table::new();

  table.set_header(vec![
    Cell::new("Name").add_attribute(Attribute::Bold),
    Cell::new("Version").add_attribute(Attribute::Bold),
  ]);

  for template in templates_info.templates {
    table.add_row(vec![
      Cell::new(&template.name),
      Cell::new(&template.version),
    ]);
  }

  println!(
    "\nInstalled templates (last updated: {}):",
    templates_info.last_updated
  );
  println!("{table}");

  Ok(())
}

pub fn is_template_installed(ctx: &AppContext, template_name: &str) -> Result<bool> {
  let templates_json = ctx.paths.templates.join("anesis-templates.json");

  let templates_info: TemplatesCache = if templates_json.exists() {
    let content = fs::read_to_string(&templates_json)?;
    serde_json::from_str(&content)?
  } else {
    TemplatesCache {
      last_updated: Utc::now().to_rfc3339(),
      templates: Vec::new(),
    }
  };

  let path = Path::new(template_name);
  if !ctx.paths.templates.join(path).exists() {
    return Ok(false);
  }

  Ok(
    templates_info
      .templates
      .iter()
      .any(|t| t.name == template_name),
  )
}