repo 0.14.13

An opinionated tool for repo management.
use std::path::PathBuf;

use clap::{Args, Subcommand, ValueEnum};
use printable_shell_command::PrintableShellCommand;

use crate::common::{
    debug::DebugPrintable,
    ecosystem::Ecosystem,
    package_manager::PackageManager,
    template_file::{TemplateFile, TemplateFileArgs, TemplateFileCommand},
};

#[derive(Args, Debug)]
pub(crate) struct BoilerplateArgs {
    #[command(subcommand)]
    command: BoilerplateCommand,
}

#[derive(Debug, Subcommand)]
enum BoilerplateCommand {
    /// Set up a CI template for GitHub or Codeberg and open for editing.
    CI(CIArgs),
    /// Set up a CI template for auto-publishing releases from tags pushed to GitHub, at: .github/workflows/publish-github-release.yaml
    AutoPublishGithubRelease(TemplateFileArgs),
    /// Set up linting using Biome
    Biome(TemplateFileArgs),
    /// Set up `tsconfig.json`
    Tsconfig(TemplateFileArgs<TsconfigArgs>),
    /// Set up `readme-cli-help.json`
    ReadmeCliHelp(TemplateFileArgs),
    /// Set up `bunfig.toml`
    Bunfig(TemplateFileArgs),
    /// Set up `rust-toolchain.toml`
    RustToolchain(TemplateFileArgs),
}

#[derive(Debug, Clone, ValueEnum, Default)]
enum VCSForge {
    #[default]
    #[clap(name = "github")]
    GitHub,
    Codeberg,
}

#[derive(Args, Debug)]
pub(crate) struct CIArgs {
    #[clap(long)]
    forge: VCSForge,

    #[command(flatten)]
    template_file_args: TemplateFileArgs,
}

#[derive(Args, Clone, Debug)]
pub(crate) struct TsconfigArgs {
    #[clap(long)]
    no_dom: bool,
    #[clap(long)]
    module: Option<ESModule>,
}

#[derive(Debug, Clone, ValueEnum, Default)]
enum ESModule {
    #[default]
    ES2022,
    ES2024,
}

fn ci_template(forge: &VCSForge) -> TemplateFile<'static> {
    match forge {
        VCSForge::GitHub => {
            let bytes = include_bytes!("../templates/.github/workflows/CI.yaml");
            TemplateFile {
                relative_path: PathBuf::from("./.github/workflows/CI.yaml"),
                bytes,
            }
        }
        VCSForge::Codeberg => {
            let bytes = include_bytes!("../templates/.woodpecker/CI.yaml");
            TemplateFile {
                relative_path: PathBuf::from("./.woodpecker/CI.yaml"),
                bytes,
            }
        }
    }
}

fn publish_github_release_template() -> TemplateFile<'static> {
    let bytes = include_bytes!("../templates/.github/workflows/publish-github-release.yaml");
    TemplateFile {
        relative_path: PathBuf::from("./.github/workflows/publish-github-release.yaml"),
        bytes,
    }
}

fn biome_json_template() -> TemplateFile<'static> {
    let bytes = include_bytes!("../templates/biome.json");
    TemplateFile {
        relative_path: PathBuf::from("./biome.json"),
        bytes,
    }
}

fn tsconfig_template(
    template_file_command: TemplateFileCommand<TsconfigArgs>,
) -> TemplateFile<'static> {
    let bytes: &[u8] = {
        if let TemplateFileCommand::Add(template_file_create_args) = template_file_command {
            match (
                template_file_create_args.custom_args.no_dom,
                template_file_create_args
                    .custom_args
                    .module
                    .unwrap_or_default(),
            ) {
                (false, ESModule::ES2022) => include_bytes!("../templates/tsconfig.es2022.json"),
                (false, ESModule::ES2024) => include_bytes!("../templates/tsconfig.es2024.json"),
                (true, ESModule::ES2022) => {
                    include_bytes!("../templates/tsconfig.es2022.no-dom.json")
                }
                (true, ESModule::ES2024) => {
                    include_bytes!("../templates/tsconfig.es2024.no-dom.json")
                }
            }
        } else {
            // TODO: re-architect the need for this fallback.
            include_bytes!("../templates/tsconfig.es2022.json")
        }
    };
    TemplateFile {
        relative_path: PathBuf::from("./tsconfig.json"),
        bytes,
    }
}

fn bunfig_template() -> TemplateFile<'static> {
    let bytes = include_bytes!("../templates/bunfig.toml");
    TemplateFile {
        relative_path: PathBuf::from("./bunfig.toml"),
        bytes,
    }
}

fn readme_cli_help_template() -> TemplateFile<'static> {
    let bytes = include_bytes!("../templates/.config/readme-cli-help.json");
    TemplateFile {
        relative_path: PathBuf::from("./.config/readme-cli-help.json"),
        bytes,
    }
}

fn rust_toolchain_template() -> TemplateFile<'static> {
    let bytes = include_bytes!("../templates/rust-toolchain.toml");
    TemplateFile {
        relative_path: PathBuf::from("./rust-toolchain.toml"),
        bytes,
    }
}

fn add_biome(template_file_args: TemplateFileArgs) {
    let (binary, args, biome_command_prefix) =
        match PackageManager::auto_detect_preferred_package_manager_for_ecosystem(
            Ecosystem::JavaScript,
        ) {
            // TODO: generalize to a function to add a dependency
            Some(PackageManager::Npm) => (
                "npm",
                [
                    "install",
                    "--save-dev",
                    "@biomejs/biome",
                    "@cubing/dev-config",
                ],
                "bun x @biomejs/biome",
            ),
            Some(PackageManager::Bun) => (
                "bun",
                [
                    "add",
                    "--development",
                    "@biomejs/biome",
                    "@cubing/dev-config",
                ],
                "bun x @biomejs/biome",
            ),
            Some(PackageManager::Yarn) => (
                "yarn",
                ["add", "--dev", "@biomejs/biome", "@cubing/dev-config"],
                "npx yarn exec @biomejs/biome",
            ),
            Some(PackageManager::Pnpm) => (
                "pnpm",
                [
                    "install",
                    "--save-dev",
                    "@biomejs/biome",
                    "@cubing/dev-config",
                ],
                "npx pnpm exec biome",
            ),
            Some(PackageManager::Cargo) => panic!("unrechachable"),
            None => {
                panic!("No JS package detected.")
            }
        };
    PrintableShellCommand::new(binary)
        .arg_each(args)
        .debug_print()
        .spawn()
        .expect("Could not add development dependency")
        .wait()
        .unwrap();
    biome_json_template().handle_command(template_file_args);
    println!(
        "Use the following commands:

`package.json`:

\"lint\": \"{} check\"
\"format\": \"{} check --write\"

`Makefile` (make sure to convert ⇥ to tab indentation):

.PHONY: lint
lint:
⇥{} check

.PHONY: format
format:
⇥{} check --write
",
        biome_command_prefix, biome_command_prefix, biome_command_prefix, biome_command_prefix,
    )
}

fn add_tsconfig(template_file_args: TemplateFileArgs<TsconfigArgs>) {
    // Note that we don't install the `typescript` package because
    // `tsconfig.json` is still needed to get VS Code's built-in TypeScript
    // annotations to accept some well-established features like top-level
    // `await` (even if the project itself doesn't use `tsc`).
    let (binary, args) = match PackageManager::auto_detect_preferred_package_manager_for_ecosystem(
        Ecosystem::JavaScript,
    ) {
        // TODO: generalize to a function to add a dependency
        Some(PackageManager::Npm) => ("npm", ["install", "--save-dev", "@cubing/dev-config"]),
        Some(PackageManager::Bun) => ("bun", ["add", "--development", "@cubing/dev-config"]),
        Some(PackageManager::Yarn) => ("yarn", ["add", "--dev", "@cubing/dev-config"]),
        Some(PackageManager::Pnpm) => ("pnpm", ["install", "--save-dev", "@cubing/dev-config"]),
        Some(PackageManager::Cargo) => panic!("unrechachable"),
        None => {
            panic!("No JS package detected.")
        }
    };
    PrintableShellCommand::new(binary)
        .arg_each(args)
        .spawn()
        .expect("Could not add development dependency")
        .wait()
        .unwrap();
    tsconfig_template(template_file_args.command.clone()).handle_command(template_file_args);
    // TODO: print `tsc` invocation (requires installation)
}

fn add_bunfig(template_file_args: TemplateFileArgs) {
    bunfig_template().handle_command(template_file_args);
    // TODO: print `tsc` invocation (requires installation)
}

fn add_readme_cli_help(template_file_args: TemplateFileArgs) {
    readme_cli_help_template().handle_command(template_file_args);
    // TODO: print `readme-cli-help` invocation
}

fn add_rust_toolchain(template_file_args: TemplateFileArgs) {
    rust_toolchain_template().handle_command(template_file_args);
    // TODO: mention `test-cargo-doc`?
    println!(
        "Use the following commands:

`Makefile` (make sure to convert ⇥ to tab indentation):

.PHONY: lint-rust
lint-rust:
⇥cargo clippy -- --deny warnings
⇥cargo fmt --check


.PHONY: format-rust
format-rust:
⇥cargo clippy --fix --allow-no-vcs
⇥cargo fmt
"
    )
}

// TODO: use traits to abstract across ecosystems
pub(crate) fn boilerplate(boilerplate_args: BoilerplateArgs) {
    match boilerplate_args.command {
        BoilerplateCommand::CI(ci_args) => {
            ci_template(&ci_args.forge).handle_command(ci_args.template_file_args);
        }
        BoilerplateCommand::AutoPublishGithubRelease(template_file_args) => {
            publish_github_release_template().handle_command(template_file_args);
        }
        BoilerplateCommand::Biome(template_file_args) => add_biome(template_file_args),
        BoilerplateCommand::Tsconfig(template_file_args) => add_tsconfig(template_file_args),
        BoilerplateCommand::Bunfig(template_file_args) => add_bunfig(template_file_args),
        BoilerplateCommand::ReadmeCliHelp(template_file_args) => {
            add_readme_cli_help(template_file_args)
        }
        BoilerplateCommand::RustToolchain(template_file_args) => {
            add_rust_toolchain(template_file_args)
        }
    };
}