gaji 0.3.2

Type-safe GitHub Actions workflows in TypeScript
Documentation
use std::path::PathBuf;
use std::time::Instant;

use anyhow::Result;
use clap::Parser;
use colored::Colorize;

use gaji::builder::WorkflowBuilder;
use gaji::cache::Cache;
use gaji::cli::{Cli, Commands};
use gaji::config::Config;
use gaji::generator::TypeGenerator;
use gaji::init::{self, InitOptions};
use gaji::parser;
use gaji::watcher;

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Init {
            force,
            skip_examples,
            migrate,
            interactive,
        } => {
            cmd_init(force, skip_examples, migrate, interactive).await?;
        }
        Commands::Dev { dir, watch } => {
            cmd_dev(&dir, watch).await?;
        }
        Commands::Build {
            input,
            output,
            dry_run,
        } => {
            cmd_build(&input, &output, dry_run).await?;
        }
        Commands::Add { action } => {
            cmd_add(&action).await?;
        }
        Commands::Clean { cache } => {
            cmd_clean(cache).await?;
        }
    }

    Ok(())
}

async fn cmd_init(
    force: bool,
    skip_examples: bool,
    migrate: bool,
    interactive: bool,
) -> Result<()> {
    let root = std::env::current_dir()?;
    let options = InitOptions {
        force,
        skip_examples,
        migrate,
        interactive,
    };
    init::init_project(&root, options).await
}

async fn cmd_dev(dir: &str, watch: bool) -> Result<()> {
    println!("{} Starting development mode...\n", "๐Ÿš€".green());

    let config = Config::load()?;
    let token = config.resolve_token();
    let api_url = config.resolve_api_url();

    // Initial scan
    let path = PathBuf::from(dir);
    if path.exists() {
        let results = parser::analyze_directory(&path).await?;

        let mut all_refs = std::collections::HashSet::new();
        for refs in results.values() {
            all_refs.extend(refs.clone());
        }

        if !all_refs.is_empty() {
            println!(
                "{} Found {} action reference(s), generating types...",
                "๐Ÿ”".cyan(),
                all_refs.len()
            );

            let gen_start = Instant::now();
            let cache = Cache::load_or_create()?;
            let generator = TypeGenerator::with_cache_ttl(
                cache,
                PathBuf::from("generated"),
                token,
                api_url,
                config.build.cache_ttl_days,
            );
            generator.generate_types_for_refs(&all_refs).await?;

            println!(
                "{} Types generated in {:.2}s!\n",
                "โœจ".green(),
                gen_start.elapsed().as_secs_f64()
            );
        }
    }

    if watch {
        // Start watching
        watcher::watch_directory(&path).await?;
    } else {
        println!("{} Done. Run with --watch to keep watching.", "โœ“".green());
    }

    Ok(())
}

async fn cmd_build(input: &str, output: &str, dry_run: bool) -> Result<()> {
    let start = Instant::now();

    if dry_run {
        println!("{} Dry run: previewing workflows...\n", "๐Ÿ”จ".cyan());
    } else {
        println!("{} Building workflows...\n", "๐Ÿ”จ".cyan());
    }

    let builder = WorkflowBuilder::new(PathBuf::from(input), PathBuf::from(output), dry_run);

    let built = builder.build_all().await?;

    let elapsed = start.elapsed();
    if built.is_empty() {
        println!("{} No workflows built", "โš ๏ธ".yellow());
    } else {
        println!(
            "\n{} Built {} workflow(s) in {:.2}s",
            "โœ…".green(),
            built.len(),
            elapsed.as_secs_f64()
        );
    }

    Ok(())
}

async fn cmd_add(action: &str) -> Result<()> {
    let start = Instant::now();
    println!("{} Adding action: {}\n", "๐Ÿ“ฆ".cyan(), action);

    let config = Config::load()?;
    let token = config.resolve_token();
    let api_url = config.resolve_api_url();

    let cache = Cache::load_or_create()?;
    let generator = TypeGenerator::with_cache_ttl(
        cache,
        PathBuf::from("generated"),
        token,
        api_url,
        config.build.cache_ttl_days,
    );

    let mut refs = std::collections::HashSet::new();
    refs.insert(action.to_string());

    match generator.generate_types_for_refs(&refs).await {
        Ok(files) => {
            for file in files {
                println!("{} Generated {}", "โœ…".green(), file.display());
            }
            println!(
                "\n{} Done in {:.2}s",
                "โœจ".green(),
                start.elapsed().as_secs_f64()
            );
        }
        Err(e) => {
            eprintln!("{} Failed to generate types: {}", "โŒ".red(), e);
            return Err(e);
        }
    }

    Ok(())
}

async fn cmd_clean(clean_cache: bool) -> Result<()> {
    println!("{} Cleaning generated files...\n", "๐Ÿงน".cyan());

    // Remove generated directory
    if PathBuf::from("generated").exists() {
        tokio::fs::remove_dir_all("generated").await?;
        println!("{} Removed generated/", "โœ“".green());
    }

    // Optionally clean cache
    if clean_cache {
        let cache = Cache::load_or_create()?;
        cache.clear()?;
        println!("{} Cleared cache", "โœ“".green());
    }

    println!("\n{} Clean complete!", "โœจ".green());

    Ok(())
}