cargo-hatch 0.4.1

Hatch new projects like a chick coming out of its egg.
Documentation
use std::{
    fs::OpenOptions,
    io::{self, Write},
};

use anyhow::{ensure, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use clap::{Args, CommandFactory, Parser, ValueHint};
use clap_complete::Shell;

#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cli {
    #[command(subcommand)]
    Hatch(Command),
}

#[derive(Parser)]
#[command(about, author, version)]
pub enum Command {
    /// Initialize a new template with a sample configuration.
    Init {
        /// Name of the new template, using the current working directory if omitted.
        name: Option<String>,
    },
    /// List all configured bookmarks with name and description.
    List,
    /// Create a new project from configured bookmarks.
    New {
        /// Bookmark as defined in the global configuration.
        bookmark: String,
        #[command(flatten)]
        flags: CreationFlags,
    },
    /// Create a new project from a template located in a remote Git repository.
    Git {
        /// An optional sub-folder within the repository that contains the template.
        #[arg(long)]
        folder: Option<Utf8PathBuf>,
        /// HTTP or Git URL to the remote repository.
        url: String,
        #[command(flatten)]
        flags: CreationFlags,
    },
    /// Create a new project from a template located in the local file system.
    Local {
        /// Location of the template directory.
        #[arg(value_hint = ValueHint::DirPath)]
        path: Utf8PathBuf,
        #[command(flatten)]
        flags: CreationFlags,
    },
    /// Generate auto-completion scripts for various shells.
    Completions {
        /// Shell to generate an auto-completion script for.
        #[arg(value_enum)]
        shell: Shell,
    },
    /// Generate man pages into the given directory.
    Manpages {
        /// Target directory, that must already exist and be empty. If the any file with the same
        /// name as any of the man pages already exist, it'll not be overwritten, but instead an
        /// error be returned.
        #[arg(value_hint = ValueHint::DirPath)]
        dir: Utf8PathBuf,
    },
}

#[derive(Args)]
pub struct CreationFlags {
    /// Name of the new project, using the current working directory if omitted.
    ///
    /// If the name contains slashes `/`, it is treated as a file path and the target directory is
    /// created automatically if missing. The final project name will become the last part of the
    /// path.
    pub name: Option<Utf8PathBuf>,
    /// Update all dependencies to the latest compatible version after project creation.
    #[arg(short, long)]
    pub update_deps: bool,
}

#[must_use]
pub fn parse() -> Command {
    let Cli::Hatch(cmd) = Cli::parse();
    cmd
}

/// Generate shell completions, written to the standard output.
pub fn completions(shell: Shell) {
    clap_complete::generate(
        shell,
        &mut Cli::command(),
        env!("CARGO_PKG_NAME"),
        &mut io::stdout().lock(),
    );
}

/// Generate man pages in the target directory. The directory must already exist and none of the
/// files exist, or an error is returned.
pub fn manpages(dir: &Utf8Path) -> Result<()> {
    fn print(dir: &Utf8Path, app: &clap::Command) -> Result<()> {
        let name = app.get_display_name().unwrap_or_else(|| app.get_name());
        let out = dir.join(format!("{name}.1"));
        let mut out = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&out)
            .with_context(|| format!("the file `{out}` already exists"))?;

        clap_mangen::Man::new(app.clone()).render(&mut out)?;
        out.flush()?;

        for sub in app.get_subcommands() {
            print(dir, sub)?;
        }

        Ok(())
    }

    ensure!(dir.try_exists()?, "target directory doesn't exist");

    let mut app = Command::command();
    app.build();

    print(dir, &app)
}

#[cfg(test)]
mod tests {
    use super::Cli;

    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Cli::command().debug_assert();
    }
}