guild-cli 0.1.0

Rust-native polyglot monorepo orchestrator
Documentation
mod cli;

use anyhow::Result;
use clap::Parser;

use cli::{CacheCommand, Cli, Commands};
use guild_cli::{
    Cache, ProjectGraph, WorkspaceConfig, discover_projects, find_workspace_root, print_error,
    print_header, print_project_entry, print_success, print_warning, run_affected, run_init,
    run_target,
};

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

    if let Err(e) = run(cli).await {
        print_error(&format!("{e}"));
        std::process::exit(1);
    }
}

async fn run(cli: Cli) -> Result<()> {
    match cli.command {
        None => {
            use clap::CommandFactory;
            Cli::command().print_help()?;
            println!();
        }

        Some(Commands::List) => {
            let cwd = std::env::current_dir()?;
            let root = find_workspace_root(&cwd)?;
            let workspace = WorkspaceConfig::from_file(&root.join("guild.toml"))?;
            let projects = discover_projects(&workspace)?;

            print_header(&format!("Workspace: {}", workspace.name()));
            println!("  {} projects discovered\n", projects.len());

            for project in &projects {
                print_project_entry(
                    project.name().as_str(),
                    &project.root().display().to_string(),
                    project.tags(),
                );
            }
        }

        Some(Commands::Graph) => {
            let cwd = std::env::current_dir()?;
            let root = find_workspace_root(&cwd)?;
            let workspace = WorkspaceConfig::from_file(&root.join("guild.toml"))?;
            let projects = discover_projects(&workspace)?;
            let graph = ProjectGraph::build(projects)?;

            print_header("Project Dependency Graph");
            let order = graph.topological_order()?;
            for name in &order {
                let deps = graph.dependencies(name).unwrap();
                if deps.is_empty() {
                    println!("  {name}");
                } else {
                    let dep_names: Vec<String> = deps.iter().map(|d| d.to_string()).collect();
                    println!("  {name} -> {}", dep_names.join(", "));
                }
            }
        }

        Some(Commands::Dev) => {
            let cwd = std::env::current_dir()?;
            let result = run_target(&cwd, "dev", None).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Build) => {
            let cwd = std::env::current_dir()?;
            let result = run_target(&cwd, "build", None).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Test) => {
            let cwd = std::env::current_dir()?;
            let result = run_target(&cwd, "test", None).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Lint) => {
            let cwd = std::env::current_dir()?;
            let result = run_target(&cwd, "lint", None).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Run { target, project }) => {
            let cwd = std::env::current_dir()?;
            let result = run_target(&cwd, &target, project.as_deref()).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Affected { target, base }) => {
            let cwd = std::env::current_dir()?;
            let result = run_affected(&cwd, &target, &base).await?;
            if !result.is_success() {
                std::process::exit(1);
            }
        }
        Some(Commands::Cache { command }) => {
            let cwd = std::env::current_dir()?;
            let root = find_workspace_root(&cwd)?;
            let cache = Cache::new(&root);

            match command {
                CacheCommand::Status => {
                    let stats = cache.stats()?;
                    print_header("Cache Status");
                    println!("  Cache directory: {}", cache.cache_dir().display());
                    println!("  Entries: {}", stats.entry_count);
                    println!("  Total size: {}", format_size(stats.total_size));
                }
                CacheCommand::Clean => {
                    if !cache.cache_dir().exists() {
                        print_warning("Cache directory does not exist, nothing to clean");
                    } else {
                        let removed = cache.clean()?;
                        print_success(&format!("Removed {removed} cache entries"));
                    }
                }
            }
        }
        Some(Commands::Init { yes }) => {
            let cwd = std::env::current_dir()?;
            let workspace_name = cwd
                .file_name()
                .map(|s| s.to_string_lossy().to_string())
                .unwrap_or_else(|| "workspace".to_string());

            print_header(&format!("Initializing Guild workspace: {workspace_name}"));

            let result = run_init(&cwd, &workspace_name, yes)?;

            println!();
            if result.written.is_empty() && result.skipped.is_empty() {
                print_success(
                    "No projects detected. Create project manifests first (package.json, Cargo.toml, go.mod, or pyproject.toml).",
                );
            } else {
                print_success(&format!(
                    "Initialized {} guild.toml file(s), skipped {} existing",
                    result.written.len(),
                    result.skipped.len()
                ));
            }
        }
    }

    Ok(())
}

fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes} bytes")
    }
}