crax 0.1.8

An interesting CLI for frontend programmer
Documentation
use anyhow::anyhow;
use clap::Subcommand;
use crax_monorepo::{
    build::BuildOptions,
    ctx::{self, write_sha},
    dev::DevOptions,
    monorepo,
    release::ReleaseOptions,
    robot::{Robot, commit, get_sha},
    share::{is_clear, unwrap_ref_option},
};
use owo_colors::OwoColorize;
use which::which;

#[derive(Subcommand, Clone)]
pub enum Cmd {
    #[command(about = "Update or check monorepo dependence's states")]
    Audit {
        #[arg(
            short,
            long,
            default_value_t = false,
            help = "Whether update pnpm-workspace.yaml or not"
        )]
        update: bool,
        #[arg(
            long,
            help = "catalog remote url or local file path, eg: http://a/b/catalog.yaml or absolute path: /a/b/catalog.yaml"
        )]
        uri: Option<String>,
        #[arg(
            long,
            help = "catalog remote url tag, eg: uri is http://a/b/catalog.yaml and tag is rc will get final uri is: http://a/b/catalog-rc.yaml"
        )]
        tag: Option<String>,
    },
    #[command(long_about = "See all packages information")]
    Info {
        #[arg(short = 'd', long, help = "See short message")]
        detail: bool,
    },
    #[command(long_about = "See diff message for every package")]
    Diff,
    #[command(long_about = "Build packages")]
    Build {
        #[arg(short = 'S', long = "no-select", help = "Avoid select dialog")]
        no_select: bool,
        #[arg(short, long, value_delimiter = ',', help = "Build Packages names")]
        names: Option<Vec<String>>,
        #[arg(long, help = "Select by diff")]
        diff: bool,
        #[arg(long, help = "Do not crawl all workspace dependence")]
        single: bool,
        #[arg(long, help = "See builded detail")]
        detail: bool,
    },
    #[command(long_about = "Release packages")]
    Release {
        #[arg(short = 'S', long = "no-select", help = "Avoid select dialog")]
        no_select: bool,
        #[arg(short = 'a', long, help = "Release all packages")]
        all: bool,
        #[arg(short, long, value_delimiter = ',', help = "Build Packages names")]
        names: Option<Vec<String>>,
        #[arg(short, long, value_delimiter = ',', help = "Npm registries")]
        reg: Option<Vec<String>>,
        #[arg(short = 'p', long = "pre", help = "Pre-patch version")]
        pre: bool,
        #[arg(
            long = "pre-tag",
            help = "Define your own pre publish tag e.g. alpha, beta, rc, default is rc"
        )]
        pre_tag: Option<String>,
        #[arg(long, help = "Npm release tag default is latest")]
        tag: Option<String>,
        #[arg(
            short,
            long = "dry-run",
            help = "Does everything a publish would do except actually publishing to the registry"
        )]
        dry_run: bool,
        #[arg(long, help = "Release by diff")]
        diff: bool,
        #[arg(long, help = "Push to remote origin")]
        push: bool,
        #[arg(long, help = "Personal Access Token")]
        pat: Option<String>,
        #[arg(long, help = "Keep the current version")]
        keep: bool,
        #[arg(long = "no-build", help = "Skip build")]
        no_build: bool,
        #[arg(long, help = "See release and build detail")]
        detail: bool,
        #[arg(long, long = "no-git-check", help = "Ignore git status check")]
        no_git_check: bool, // some build command generate some changes, but those changes should be ignored
    },
    #[command(long_about = "Run dev")]
    Dev {
        #[arg(short, long, value_delimiter = ',', help = "Npm registries")]
        names: Option<Vec<String>>,
    },
}

pub async fn run(ctx: &mut ctx::Ctx, command: Option<Cmd>, mut root: bool) -> anyhow::Result<()> {
    let pnpm_error_msg =
        "❌ Can not find pnpm, Please install it first.".bold().yellow().to_string();
    if let Some(include_root) = ctx.config.include_root_project {
        root = include_root
    }
    let mut mono = monorepo::Monorepo::new(&ctx.cwd, ctx.config.ignore.as_ref(), root);
    let config = &ctx.config;

    if let Some(ref meta) = config.meta {
        mono.sort_and_init(meta).unwrap();
    }

    let pre_build_commit = unwrap_ref_option(&config.pre_build_commit, "HEAD^".into());
    let diff_ignore = &config.diff_ignore;
    let dts_ignore = &config.dts_ignore.clone().unwrap_or(vec![]);

    match command {
        Some(Cmd::Info { detail }) => Ok(mono.info(detail)),
        Some(Cmd::Diff) => {
            let _ = mono.diff(&pre_build_commit, diff_ignore, true).await?;
            Ok(())
        }
        Some(Cmd::Audit { update, uri, tag }) => {
            which("pnpm").expect(&pnpm_error_msg);

            let remote_url = uri.or_else(|| ctx.config.catalog_remote_url.clone());

            if let Some(uri) = remote_url {
                Ok(mono.audit(&uri, update, tag).await?)
            } else {
                Err(anyhow!("Can not find any valid remote uri.".red()))
            }
        }
        Some(Cmd::Build { no_select, names, diff, single, detail }) => {
            which("pnpm").expect(&pnpm_error_msg);
            let names = if diff {
                Some(mono.diff(&pre_build_commit, diff_ignore, true).await?)
            } else {
                names.clone()
            };

            // mono.build(names, !no_select, dts_ignore, single, false).await?;
            mono.build(BuildOptions {
                names,
                select: !no_select,
                dts_ignore: Some(dts_ignore),
                single,
                avoid_run: false,
                detail,
            })
            .await?;

            Ok(())
        }
        Some(Cmd::Release {
            no_select,
            names,
            pre,
            reg,
            all,
            dry_run,
            diff,
            push,
            pat,
            keep,
            no_build,
            pre_tag,
            tag,
            detail,
            no_git_check,
        }) => {
            let should_build_all = mono.should_build_all().await;
            let mut package_names: Option<Vec<String>>;
            let is_all = all || should_build_all;

            if diff && !no_git_check {
                is_clear(&ctx.cwd).await?;
            }

            package_names = if diff && !is_all {
                Some(mono.diff(&pre_build_commit, diff_ignore, true).await?)
            } else {
                names.clone()
            };

            let registries = reg
                .unwrap_or_else(|| ctx.config.registries.clone().expect("Please set registries!!"));

            if should_build_all {
                println!(
                    "{}\n",
                    "Due to the discovery of [*], all packages will be released.".yellow()
                );
            }

            if is_all {
                package_names = Some(mono.names());
            }

            if let Some(released_pkgs) = mono
                .release(ReleaseOptions {
                    names: package_names,
                    registries: Some(&registries),
                    pre,
                    select: !no_select,
                    dry_run,
                    keep,
                    dts_ignore: Some(dts_ignore),
                    detail,
                    skip_build: no_build,
                    pre_tag: pre_tag.as_deref(),
                    tag,
                })
                .await?
            {
                let re_pkgs_len = released_pkgs.len();

                if !keep && !released_pkgs.is_empty() && !dry_run {
                    let mes = released_pkgs
                        .iter()
                        .map(|s| format!("{}@{}", s, mono.get(s).current_version().unwrap()))
                        .collect::<Vec<String>>()
                        .join(" ");

                    let sha = get_sha(&ctx.cwd).await?;
                    write_sha(&sha, &ctx.cwd)?;

                    println!("{}", "has updated sha!".green());

                    let re_mes = if re_pkgs_len <= 1 { "package" } else { "packages" };

                    if push {
                        println!("{}", "start pushing...".yellow());

                        if let Some(v) = &config.git_path {
                            if let Some(p) = pat {
                                let robot = Robot::new(v, &p, "ci_bot", "ci_bot@runa.com");

                                robot.init(&ctx.cwd).await?;

                                commit(
                                    &format!(
                                        "chore(release): 🚀 {} {} [skip ci] \n\n{}",
                                        re_pkgs_len, re_mes, &mes
                                    ),
                                    &ctx.cwd,
                                )
                                .await?;

                                robot.push(&ctx.cwd).await?;
                            } else {
                                return Err(anyhow::Error::msg("Can not find git PAT!"));
                            }
                        } else {
                            return Err(anyhow::Error::msg("Can not find git_path"));
                        }

                        println!("{}\n", "push successful!".green());
                    } else {
                        commit(
                            &format!(
                                "chore(release): 🚀 {} {} [skip ci] \n\n{}",
                                re_pkgs_len, re_mes, &mes
                            ),
                            &ctx.cwd,
                        )
                        .await?;
                    }
                }
            }

            Ok(())
        }
        Some(Cmd::Dev { names }) => {
            which("pnpm").expect(&pnpm_error_msg);
            mono.dev(DevOptions { names }).await?;
            Ok(())
        }
        None => Err(anyhow::Error::msg("Command not found!")),
    }
}