Skip to content

Commit b9b643a

Browse files
committed
feat: v0.1.5
1 parent c2b9049 commit b9b643a

File tree

11 files changed

+189
-1
lines changed

11 files changed

+189
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exclude = ["target", "Cargo.lock", "sh", ".github"]
1313

1414
[dependencies]
1515
toml = "0.9.8"
16+
thiserror = "2.0.18"
1617
tokio = { version = "1.49.0", features = ["full"] }
1718

1819
[profile.dev]

src/command/enum.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub(crate) enum CommandType {
99
Bump,
1010
/// Publish packages in monorepo
1111
Publish,
12+
/// Create a new project from template
13+
New,
1214
/// Show help
1315
Help,
1416
/// Show version

src/config/fn.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) fn parse_args() -> Args {
1212
let mut manifest_path: Option<String> = None;
1313
let mut bump_type: Option<BumpVersionType> = None;
1414
let mut max_retries: u32 = 3;
15+
let mut project_name: Option<String> = None;
1516
let mut i: usize = 1;
1617
while i < raw_args.len() {
1718
let arg: &str = raw_args[i].as_str();
@@ -42,6 +43,17 @@ pub(crate) fn parse_args() -> Args {
4243
command = CommandType::Publish;
4344
}
4445
}
46+
"new" => {
47+
if command == CommandType::Help || command == CommandType::Version {
48+
command = CommandType::New;
49+
i += 1;
50+
if i < raw_args.len() && !raw_args[i].starts_with("--") && !raw_args[i].starts_with("-") {
51+
project_name = Some(raw_args[i].clone());
52+
} else {
53+
i -= 1;
54+
}
55+
}
56+
}
4557
"--patch" => {
4658
bump_type = Some(BumpVersionType::Patch);
4759
}
@@ -90,5 +102,6 @@ pub(crate) fn parse_args() -> Args {
90102
manifest_path,
91103
bump_type,
92104
max_retries,
105+
project_name,
93106
}
94107
}

src/config/struct.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ pub struct Args {
1313
pub bump_type: Option<BumpVersionType>,
1414
/// Maximum retry attempts for publish command
1515
pub max_retries: u32,
16+
/// Project name for new command
17+
pub project_name: Option<String>,
1618
}

src/help/fn.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ pub(crate) fn print_help() {
77
println!(" fmt Format Rust code using cargo fmt");
88
println!(" watch Watch files and run cargo run using cargo-watch");
99
println!(" publish Publish packages in monorepo with topological ordering");
10+
println!(" new Create a new project from template");
1011
println!(" -h, --help Print this help message");
1112
println!(" -v, --version Print version information");
1213
println!();
14+
println!("New Options:");
15+
println!(" <PROJECT_NAME> Name of the project to create");
16+
println!();
1317
println!("Bump Options:");
1418
println!(" --patch Bump patch version (0.1.2 -> 0.1.3) [default]");
1519
println!(" --minor Bump minor version (0.1.2 -> 0.2.0)");

src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ mod command;
77
mod config;
88
mod fmt;
99
mod help;
10+
mod new;
1011
mod publish;
1112
mod version;
1213
mod watch;
1314

1415
pub(crate) use {
15-
bump::*, command::*, config::*, fmt::*, help::*, publish::*, version::*, watch::*,
16+
bump::*, command::*, config::*, fmt::*, help::*, new::*, publish::*, version::*, watch::*,
1617
};
1718

1819
pub(crate) use std::{
@@ -80,6 +81,17 @@ async fn main() {
8081
}
8182
}
8283
}
84+
CommandType::New => {
85+
if let Some(project_name) = args.project_name {
86+
if let Err(error) = execute_new(&project_name).await {
87+
eprintln!("new failed: {error}");
88+
exit(1);
89+
}
90+
} else {
91+
eprintln!("Error: Project name is required. Usage: hyperlane-cli new <PROJECT_NAME>");
92+
exit(1);
93+
}
94+
}
8395
CommandType::Help => print_help(),
8496
CommandType::Version => print_version(),
8597
}

src/new/enum.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// Errors that can occur during project creation
2+
#[derive(Debug, thiserror::Error)]
3+
pub(crate) enum NewError {
4+
/// IO error occurred
5+
#[error("IO error: {0}")]
6+
IoError(#[from] std::io::Error),
7+
/// Git command not found
8+
#[error("Git is not installed or not found in PATH")]
9+
GitNotFound,
10+
/// Project already exists
11+
#[error("Project directory '{0}' already exists")]
12+
ProjectExists(String),
13+
/// Git clone failed
14+
#[error("Git clone failed: {0}")]
15+
CloneFailed(String),
16+
/// Invalid project name
17+
#[error("Invalid project name: {0}")]
18+
InvalidName(String),
19+
}

src/new/fn.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use crate::*;
2+
3+
/// Validate project name
4+
///
5+
/// # Arguments
6+
///
7+
/// - `&str`: Project name to validate
8+
///
9+
/// # Returns
10+
///
11+
/// - `Result<(), NewError>`: Ok if valid, error otherwise
12+
fn validate_project_name(name: &str) -> Result<(), NewError> {
13+
if name.is_empty() {
14+
return Err(NewError::InvalidName(
15+
"Project name cannot be empty".to_string(),
16+
));
17+
}
18+
if name.contains('/') || name.contains('\\') || name.contains(':') {
19+
return Err(NewError::InvalidName(
20+
"Project name contains invalid characters".to_string(),
21+
));
22+
}
23+
if name.starts_with('.') || name.starts_with('-') {
24+
return Err(NewError::InvalidName(
25+
"Project name cannot start with '.' or '-'".to_string(),
26+
));
27+
}
28+
Ok(())
29+
}
30+
31+
/// Check if git is available in the system
32+
///
33+
/// # Returns
34+
///
35+
/// - `Result<(), NewError>`: Ok if git is available, error otherwise
36+
async fn check_git_available() -> Result<(), NewError> {
37+
let output: std::process::Output = Command::new("git")
38+
.arg("--version")
39+
.stdout(Stdio::null())
40+
.stderr(Stdio::null())
41+
.output()
42+
.await
43+
.map_err(|_| NewError::GitNotFound)?;
44+
if output.status.success() {
45+
Ok(())
46+
} else {
47+
Err(NewError::GitNotFound)
48+
}
49+
}
50+
51+
/// Execute git clone command
52+
///
53+
/// # Arguments
54+
///
55+
/// - `&NewProjectConfig`: Project configuration containing template URL and project name
56+
///
57+
/// # Returns
58+
///
59+
/// - `Result<(), NewError>`: Success or error
60+
async fn git_clone(config: &NewProjectConfig) -> Result<(), NewError> {
61+
let project_path: PathBuf = PathBuf::from(&config.project_name);
62+
if project_path.exists() {
63+
return Err(NewError::ProjectExists(config.project_name.clone()));
64+
}
65+
let output: std::process::Output = Command::new("git")
66+
.arg("clone")
67+
.arg(&config.template_url)
68+
.arg(&config.project_name)
69+
.stdout(Stdio::piped())
70+
.stderr(Stdio::piped())
71+
.output()
72+
.await
73+
.map_err(|e| NewError::IoError(e))?;
74+
if output.status.success() {
75+
Ok(())
76+
} else {
77+
let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
78+
Err(NewError::CloneFailed(stderr))
79+
}
80+
}
81+
82+
/// Execute new command to create a project from template
83+
///
84+
/// # Arguments
85+
///
86+
/// - `&str`: Name of the project to create
87+
///
88+
/// # Returns
89+
///
90+
/// - `Result<(), NewError>`: Success or error
91+
pub(crate) async fn execute_new(project_name: &str) -> Result<(), NewError> {
92+
validate_project_name(project_name)?;
93+
check_git_available().await?;
94+
let config: NewProjectConfig = NewProjectConfig::new(project_name.to_string());
95+
println!(
96+
"Creating new project '{}' from template...",
97+
config.project_name
98+
);
99+
git_clone(&config).await?;
100+
println!("Successfully created project '{}'", config.project_name);
101+
println!(" cd {}", config.project_name);
102+
println!(" cargo build");
103+
Ok(())
104+
}

src/new/impl.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use crate::*;
2+
3+
impl NewProjectConfig {
4+
/// Create a new project configuration with default template
5+
///
6+
/// # Arguments
7+
/// - `project_name`: Name of the project
8+
///
9+
/// # Returns
10+
/// - `NewProjectConfig`: Configuration instance
11+
pub(crate) fn new(project_name: String) -> Self {
12+
Self {
13+
project_name,
14+
template_url: "https://github.com/hyperlane-dev/hyperlane-quick-start".to_string(),
15+
}
16+
}
17+
}

src/new/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod r#enum;
2+
mod r#fn;
3+
mod r#impl;
4+
mod r#struct;
5+
6+
pub(crate) use {r#enum::*, r#fn::*, r#struct::*};

0 commit comments

Comments
 (0)