zino_cli/cli/
mod.rs

1//! CLI arguments and subcommands.
2
3use clap::Parser;
4use git2::Repository;
5use regex::Regex;
6use std::fs;
7use std::fs::remove_dir_all;
8use std::path::Path;
9use walkdir::{DirEntry, WalkDir};
10use zino_core::error::Error;
11
12mod deploy;
13mod init;
14mod new;
15mod serve;
16
17/// CLI tool for developing Zino applications.
18#[derive(Parser)]
19#[clap(name = "zino", version)]
20pub struct Cli {
21    /// Specify the bin target.
22    #[clap(global = true, long)]
23    bin: Option<String>,
24    /// Subcommands.
25    #[clap(subcommand)]
26    action: Subcommands,
27    /// Enable verbose logging.
28    #[clap(long)]
29    verbose: bool,
30}
31
32impl Cli {
33    /// Returns the subcommand action.
34    #[inline]
35    pub fn action(self) -> Subcommands {
36        self.action
37    }
38}
39
40/// CLI subcommands.
41#[derive(Parser)]
42pub enum Subcommands {
43    /// Initialize the project.
44    Init(init::Init),
45    /// Create a new project.
46    New(new::New),
47    /// Start the server at localhost:6080/zino-config.html.
48    Serve(serve::Serve),
49    /// Deploy the project.
50    Deploy(deploy::Deploy),
51}
52
53/// Default path for temporary template.
54pub(crate) static TEMPORARY_TEMPLATE_PATH: &str = "./temporary_zino_template";
55
56/// Default template URL.
57pub(crate) static DEFAULT_TEMPLATE_URL: &str =
58    "https://github.com/zino-rs/zino-template-default.git";
59
60/// Clones the template repository, do replacements, and create the project.
61pub(crate) fn clone_and_process_template(
62    template_url: &str,
63    target_path_prefix: &str,
64    project_name: &str,
65) -> Result<(), Error> {
66    Repository::clone(template_url, TEMPORARY_TEMPLATE_PATH)?;
67
68    for entry in WalkDir::new(TEMPORARY_TEMPLATE_PATH)
69        .into_iter()
70        .filter_entry(|e| !is_ignored(e))
71    {
72        let entry = entry?;
73        if entry.file_type().is_file() {
74            let template_file_path = entry.path();
75            let target_path = format!(
76                ".{}/{}",
77                target_path_prefix,
78                template_file_path
79                    .strip_prefix(TEMPORARY_TEMPLATE_PATH)?
80                    .to_str()
81                    .ok_or_else(|| Error::new(
82                        "fail to convert the template file path to string"
83                    ))?
84            );
85            fs::create_dir_all(Path::new(&target_path).parent().unwrap())?;
86
87            let content =
88                fs::read_to_string(template_file_path)?.replace("{project-name}", project_name);
89            fs::write(&target_path, content)?;
90        }
91    }
92
93    Ok(())
94}
95
96/// Helper function to determine ignored files.
97fn is_ignored(entry: &DirEntry) -> bool {
98    entry.file_name().to_str().map_or(false, |s| {
99        s.starts_with('.') || s == "LICENSE" || s == "README.md"
100    })
101}
102
103/// Clean the temporary template directory.
104fn clean_template_dir(path: &str) {
105    let _ = remove_dir_all(path);
106}
107
108/// Check name validity.
109pub(crate) fn check_package_name_validation(name: &str) -> Result<(), Error> {
110    Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
111        .map_err(|e| Error::new(e.to_string()))?
112        .is_match(name)
113        .then_some(())
114        .ok_or_else(|| Error::new(format!("invalid package name: `{}`", name)))
115}