anesis 0.9.1

CLI for scaffolding projects from remote templates and extending them with project addons
Documentation
use std::time::Duration;

use anesis::{
  AppContext, CleanupState, addons,
  auth::{account::print_user_info, login::login, logout::logout},
  cache::{get_installed_templates, remove_template_from_cache},
  cli::{
    self,
    commands::{AddonCommands, Commands, TemplateCommands, UseCommands},
  },
  completions,
  paths::AnesisPaths,
  templates::{
    generator::extract_template,
    install::{InstallResult, install_template},
    loader::get_files,
    publish::publish,
    update::update,
  },
  upgrade::{check_cli_version_cached, render_upgrade_notice, upgrade_cli},
  utils::{
    cleanup::setup_ctrlc_handler,
    errors::print_error,
    validate::{is_valid_github_repo_url, validate_project_name, validate_template_name},
  },
};
use anyhow::{Context, Result};
use reqwest::Client;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() {
  completions::complete_env();
  if let Err(err) = run().await {
    print_error(&err);
    std::process::exit(1);
  }
}

async fn run() -> Result<()> {
  let cli = cli::parse();
  let anesis_paths = AnesisPaths::new()?;
  anesis_paths.ensure_directories()?;
  let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
  let cleanup_state: CleanupState = Arc::new(Mutex::new(None));

  setup_ctrlc_handler(cleanup_state.clone(), anesis_paths.templates.clone())?;

  let ctx = AppContext::new(anesis_paths, client, cleanup_state);
  let skip_version_notice = matches!(
    &cli.command,
    Commands::Upgrade | Commands::Completions { .. }
  );
  let version_check_handle = if skip_version_notice {
    None
  } else {
    let client = ctx.client.clone();
    let version_check_path = ctx.paths.version_check.clone();
    Some(tokio::spawn(async move {
      check_cli_version_cached(&client, &version_check_path).await
    }))
  };

  match cli.command {
    Commands::New {
      name,
      template_name,
    } => {
      validate_project_name(&name)?;
      create_new_project(&ctx, &name, &template_name).await?;
    }
    Commands::Template { command } => match command {
      TemplateCommands::Install { template_name } => {
        validate_template_name(&template_name)?;
        let install_result = install_template(&ctx, &template_name).await?;
        match install_result {
          InstallResult::UpToDate => {
            println!("{}", InstallResult::up_to_date_message(&template_name));
          }
          _ => {
            if let Some(message) = install_result.message(&template_name) {
              println!("{message}");
            }
          }
        }
      }
      TemplateCommands::List => {
        get_installed_templates(&ctx.paths.templates)?;
      }
      TemplateCommands::Remove { template_name } => {
        remove_template_from_cache(&ctx.paths.templates, &template_name)?;
      }
      TemplateCommands::Publish { template_url } => {
        is_valid_github_repo_url(&template_url)?;
        publish(&ctx, &template_url).await?;
      }
      TemplateCommands::Update { template_url } => {
        update(&ctx, &template_url).await?;
      }
    },
    Commands::Login => {
      login(&ctx.paths.auth, &ctx.backend_url, &ctx.frontend_url).await?;
    }
    Commands::Logout => {
      logout(&ctx.paths.auth)?;
    }
    Commands::Account => {
      print_user_info(&ctx).await?;
    }
    Commands::Addon { command } => match command {
      AddonCommands::Install { addon_id } => {
        let install_result = addons::install::install_addon(&ctx, &addon_id).await?;
        match &install_result {
          addons::install::AddonInstallResult::UpToDate(_) => {
            println!(
              "{}",
              addons::install::AddonInstallResult::up_to_date_message(&addon_id)
            );
          }
          _ => {
            if let Some(message) = install_result.message(&addon_id) {
              println!("{message}");
            }
          }
        }
      }
      AddonCommands::List => {
        addons::cache::get_installed_addons(&ctx.paths.addons)?;
      }
      AddonCommands::Remove { addon_id } => {
        addons::cache::remove_addon_from_cache(&ctx.paths.addons, &addon_id)?;
      }
      AddonCommands::Publish { addon_url } => {
        is_valid_github_repo_url(&addon_url)?;
        addons::publish::publish_addon(&ctx, &addon_url).await?;
      }
      AddonCommands::Update { addon_url } => {
        is_valid_github_repo_url(&addon_url)?;
        addons::update::update_addon(&ctx, &addon_url).await?;
      }
    },
    Commands::Use { command } => match command {
      UseCommands::External(args) => {
        let addon_id = args
          .first()
          .context("Usage: anesis use <addon-id> <command>")?;
        let command_name = args
          .get(1)
          .context("Usage: anesis use <addon-id> <command>")?;
        let project_root = std::env::current_dir()?;
        addons::runner::run_addon_command(&ctx, addon_id, command_name, &project_root).await?;
      }
    },
    Commands::Upgrade => {
      upgrade_cli(&ctx).await?;
    }
    Commands::Completions { shell } => {
      completions::install_completions(shell)?;
    }
  }

  if let Some(version_check_handle) = version_check_handle
    && let Ok(Ok(Some(latest_version))) = version_check_handle.await
  {
    println!("{}", render_upgrade_notice(&latest_version));
  }

  Ok(())
}

async fn create_new_project(
  ctx: &AppContext,
  project_name: &str,
  template_name: &str,
) -> Result<()> {
  let files = get_files(ctx, template_name).await?;
  extract_template(&files, project_name)?;
  println!("✅ Project created successfully!");
  println!("\nNext steps:");
  println!("  cd {}", project_name);
  Ok(())
}