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 super::manifest::AddonManifest;

#[derive(Serialize, Deserialize, Debug)]
pub struct AddonsCache {
  #[serde(rename = "lastUpdated")]
  pub last_updated: String,
  pub addons: Vec<CachedAddon>,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct CachedAddon {
  pub id: String,
  pub name: String,
  pub version: String,
  pub path: String,
  pub commit_sha: String,
  pub repo_url: String,
}

fn read_cache(addons_dir: &Path) -> Result<AddonsCache> {
  let index = addons_dir.join("anesis-addons.json");
  if index.exists() {
    let content = fs::read_to_string(&index)?;
    Ok(serde_json::from_str(&content)?)
  } else {
    Ok(AddonsCache {
      last_updated: Utc::now().to_rfc3339(),
      addons: Vec::new(),
    })
  }
}

fn write_cache(addons_dir: &Path, cache: &AddonsCache) -> Result<()> {
  let index = addons_dir.join("anesis-addons.json");
  fs::write(index, serde_json::to_string_pretty(cache)?)?;
  Ok(())
}

pub fn update_addons_cache(
  addons_dir: &Path,
  subdir: &str,
  manifest: &AddonManifest,
  commit_sha: &str,
) -> Result<()> {
  let mut cache = read_cache(addons_dir)?;

  cache.last_updated = Utc::now().to_rfc3339();
  cache.addons.retain(|a| a.id != manifest.id);
  cache.addons.push(CachedAddon {
    id: manifest.id.clone(),
    name: manifest.name.clone(),
    version: manifest.version.clone(),
    path: subdir.to_string(),
    commit_sha: commit_sha.to_string(),
    repo_url: String::new(),
  });

  write_cache(addons_dir, &cache)
}

pub fn get_cached_addon(addons_dir: &Path, addon_id: &str) -> Result<Option<CachedAddon>> {
  let cache = read_cache(addons_dir)?;
  Ok(cache.addons.into_iter().find(|a| a.id == addon_id))
}

pub fn remove_addon_from_cache(addons_dir: &Path, addon_id: &str) -> Result<()> {
  let mut cache = read_cache(addons_dir)?;

  let entry = cache
    .addons
    .iter()
    .find(|a| a.id == addon_id)
    .ok_or_else(|| anyhow::anyhow!("Addon '{}' is not installed", addon_id))?;

  let addon_dir = addons_dir.join(&entry.path);
  if addon_dir.exists() {
    fs::remove_dir_all(&addon_dir)?;
  }

  cache.last_updated = Utc::now().to_rfc3339();
  cache.addons.retain(|a| a.id != addon_id);

  write_cache(addons_dir, &cache)?;
  println!("✓ Removed addon '{}'", addon_id);
  Ok(())
}

pub fn get_installed_addons(addons_dir: &Path) -> Result<()> {
  let cache = read_cache(addons_dir)?;

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

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

  for addon in &cache.addons {
    table.add_row(vec![
      Cell::new(&addon.id),
      Cell::new(&addon.name),
      Cell::new(&addon.version),
    ]);
  }

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

  Ok(())
}

pub fn is_addon_installed(addons_dir: &Path, addon_id: &str) -> Result<bool> {
  let cache = read_cache(addons_dir)?;
  let found = cache.addons.iter().find(|a| a.id == addon_id);
  match found {
    Some(entry) => Ok(addons_dir.join(&entry.path).exists()),
    None => Ok(false),
  }
}