From 68efb4048c30e1af2159506111be587f2fb26240 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:42:48 +0000 Subject: [PATCH 1/4] Initial plan From c46d1cb74f7952a2b06acc98bb4b80774f9334ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:48:36 +0000 Subject: [PATCH 2/4] Refactor code with Result pattern, modular structure, and cross-platform support Co-authored-by: KevinArce <83199462+KevinArce@users.noreply.github.com> --- src/cli.rs | 48 +++++++++ src/error.rs | 59 ++++++++++ src/generator.rs | 274 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++ src/main.rs | 63 +---------- 5 files changed, 394 insertions(+), 58 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/error.rs create mode 100644 src/generator.rs create mode 100644 src/lib.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..4d8726c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,48 @@ +use crate::error::Result; +use crate::generator::{ProjectConfig, ProjectGenerator}; +use std::io; + +/// CLI interface for the bun-cli tool +pub struct Cli; + +impl Cli { + /// Run the CLI application + pub fn run() -> Result<()> { + println!("šŸ¦€ Bun CLI Generator"); + println!("A Cool name for your Bun project šŸ˜Ž:"); + + let project_name = Self::read_project_name()?; + + let config = ProjectConfig { + name: project_name.clone(), + ..Default::default() + }; + + let generator = ProjectGenerator::new(config); + generator.generate()?; + + println!("\n🄳 All done! Your project is ready to use."); + println!("Run 'cd {}' to get started!", project_name); + + Ok(()) + } + + /// Read project name from stdin + fn read_project_name() -> Result { + let mut project_name = String::new(); + io::stdin().read_line(&mut project_name)?; + Ok(project_name.trim().to_string()) + } + + /// Display an error message + pub fn display_error(error: &dyn std::error::Error) { + eprintln!("\nāŒ Error: {}", error); + + // Display the chain of errors if available + let mut source = error.source(); + while let Some(err) = source { + eprintln!(" Caused by: {}", err); + source = err.source(); + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..a6e3a37 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,59 @@ +use std::fmt; +use std::io; + +/// Custom error types for the bun-cli application +#[derive(Debug)] +pub enum BunCliError { + /// IO error occurred + Io(io::Error), + /// Command execution failed + CommandFailed { command: String, message: String }, + /// Invalid project name + InvalidProjectName(String), + /// Bun is not installed + BunNotInstalled, + /// Template copy failed + TemplateCopyFailed(String), + /// Dependency installation failed + DependencyFailed { dependency: String, message: String }, +} + +impl fmt::Display for BunCliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BunCliError::Io(err) => write!(f, "IO error: {}", err), + BunCliError::CommandFailed { command, message } => { + write!(f, "Command '{}' failed: {}", command, message) + } + BunCliError::InvalidProjectName(name) => { + write!(f, "Invalid project name '{}': must not be empty and should contain only valid characters", name) + } + BunCliError::BunNotInstalled => { + write!(f, "Bun is not installed. Please install Bun globally from https://bun.sh") + } + BunCliError::TemplateCopyFailed(message) => { + write!(f, "Failed to copy templates: {}", message) + } + BunCliError::DependencyFailed { dependency, message } => { + write!(f, "Failed to install dependency '{}': {}", dependency, message) + } + } + } +} + +impl std::error::Error for BunCliError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + BunCliError::Io(err) => Some(err), + _ => None, + } + } +} + +impl From for BunCliError { + fn from(err: io::Error) -> Self { + BunCliError::Io(err) + } +} + +pub type Result = std::result::Result; diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..64aa83f --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,274 @@ +use crate::error::{BunCliError, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Configuration for project generation +pub struct ProjectConfig { + pub name: String, + pub dependencies: Vec, +} + +impl Default for ProjectConfig { + fn default() -> Self { + Self { + name: String::new(), + dependencies: vec![ + "@bogeychan/elysia-logger".to_string(), + "@elysiajs/cors".to_string(), + "@elysiajs/swagger".to_string(), + "@sentry/bun".to_string(), + "@sentry/cli".to_string(), + "@types/luxon".to_string(), + "jsonwebtoken".to_string(), + "luxon".to_string(), + "mongoose".to_string(), + "winston".to_string(), + "winston-daily-rotate-file".to_string(), + ], + } + } +} + +/// Generator for creating Bun projects +pub struct ProjectGenerator { + config: ProjectConfig, +} + +impl ProjectGenerator { + /// Create a new project generator with the given configuration + pub fn new(config: ProjectConfig) -> Self { + Self { config } + } + + /// Validate the project name + fn validate_project_name(&self) -> Result<()> { + let name = self.config.name.trim(); + + if name.is_empty() { + return Err(BunCliError::InvalidProjectName( + "Project name cannot be empty".to_string(), + )); + } + + // Check for invalid characters (basic validation) + if name.contains(['/', '\\', '\0']) { + return Err(BunCliError::InvalidProjectName( + format!("Project name '{}' contains invalid characters", name), + )); + } + + Ok(()) + } + + /// Check if Bun is installed on the system + fn check_bun_installed(&self) -> Result<()> { + let output = Command::new("bun") + .arg("--version") + .output() + .map_err(|_| BunCliError::BunNotInstalled)?; + + if !output.status.success() { + return Err(BunCliError::BunNotInstalled); + } + + Ok(()) + } + + /// Create the base project using bun create + fn create_base_project(&self) -> Result<()> { + let output = Command::new("bun") + .arg("create") + .arg("elysia") + .arg(&self.config.name) + .output()?; + + if !output.status.success() { + let error_message = String::from_utf8_lossy(&output.stderr); + return Err(BunCliError::CommandFailed { + command: format!("bun create elysia {}", self.config.name), + message: error_message.to_string(), + }); + } + + Ok(()) + } + + /// Install a single dependency + fn install_dependency(&self, dep: &str) -> Result<()> { + let output = Command::new("bun") + .arg("add") + .arg(dep) + .current_dir(&self.config.name) + .output()?; + + if !output.status.success() { + let error_message = String::from_utf8_lossy(&output.stderr); + return Err(BunCliError::DependencyFailed { + dependency: dep.to_string(), + message: error_message.to_string(), + }); + } + + Ok(()) + } + + /// Install all dependencies + fn install_dependencies(&self) -> Result<()> { + for dep in &self.config.dependencies { + // Attempt to install, but continue if one fails + match self.install_dependency(dep) { + Ok(_) => println!("āœ“ Added dependency: {}", dep), + Err(e) => eprintln!("⚠ Warning: {}", e), + } + } + Ok(()) + } + + /// Copy template files to the project + fn copy_templates(&self) -> Result<()> { + // Get the source template directory + // In a binary distribution, templates would be embedded or in a known location + let template_src = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("templates") + .join("src"); + + if !template_src.exists() { + // Templates don't exist, skip copying + return Ok(()); + } + + let dest = PathBuf::from(&self.config.name).join("src"); + + // Use a cross-platform approach to copy files + Self::copy_dir_recursive(&template_src, &dest)?; + + Ok(()) + } + + /// Recursively copy directory contents (cross-platform) + fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + // Create destination directory if it doesn't exist + if !dst.exists() { + std::fs::create_dir_all(dst)?; + } + + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let file_type = entry.file_type()?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if file_type.is_dir() { + Self::copy_dir_recursive(&src_path, &dst_path)?; + } else { + // Only copy if destination doesn't exist or is different + let should_copy = !dst_path.exists() || { + let src_metadata = std::fs::metadata(&src_path)?; + let dst_metadata = std::fs::metadata(&dst_path)?; + src_metadata.len() != dst_metadata.len() + || src_metadata.modified()? > dst_metadata.modified()? + }; + + if should_copy { + std::fs::copy(&src_path, &dst_path)?; + } + } + } + + Ok(()) + } + + /// Generate the complete project + pub fn generate(&self) -> Result<()> { + // Validate project name + self.validate_project_name()?; + + // Check if Bun is installed + self.check_bun_installed()?; + + // Create base project + println!("Creating project '{}'...", self.config.name); + self.create_base_project()?; + println!("āœ“ Project '{}' created successfully", self.config.name); + + // Install dependencies + println!("Installing dependencies..."); + self.install_dependencies()?; + + // Copy templates + println!("Copying template files..."); + match self.copy_templates() { + Ok(_) => println!("āœ“ Template files copied successfully"), + Err(e) => eprintln!("⚠ Warning: {}", e), + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_empty_project_name() { + let config = ProjectConfig { + name: "".to_string(), + dependencies: vec![], + }; + let generator = ProjectGenerator::new(config); + + let result = generator.validate_project_name(); + assert!(result.is_err()); + if let Err(BunCliError::InvalidProjectName(_)) = result { + // Expected error type + } else { + panic!("Expected InvalidProjectName error"); + } + } + + #[test] + fn test_validate_project_name_with_slash() { + let config = ProjectConfig { + name: "my/project".to_string(), + dependencies: vec![], + }; + let generator = ProjectGenerator::new(config); + + let result = generator.validate_project_name(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_project_name_with_backslash() { + let config = ProjectConfig { + name: "my\\project".to_string(), + dependencies: vec![], + }; + let generator = ProjectGenerator::new(config); + + let result = generator.validate_project_name(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_valid_project_name() { + let config = ProjectConfig { + name: "my-cool-project".to_string(), + dependencies: vec![], + }; + let generator = ProjectGenerator::new(config); + + let result = generator.validate_project_name(); + assert!(result.is_ok()); + } + + #[test] + fn test_default_config_has_dependencies() { + let config = ProjectConfig::default(); + assert!(!config.dependencies.is_empty()); + assert!(config.dependencies.contains(&"@elysiajs/cors".to_string())); + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9a795db --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod cli; +pub mod error; +pub mod generator; + +// Re-export commonly used types +pub use cli::Cli; +pub use error::{BunCliError, Result}; +pub use generator::{ProjectConfig, ProjectGenerator}; diff --git a/src/main.rs b/src/main.rs index 19f7a9c..6c3d684 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,62 +1,9 @@ -use std::io; -use std::process::Command; +use bun_cli::Cli; +use std::process; fn main() { - println!("A Cool name for your Bun project šŸ˜Ž:"); - - let mut project_name = String::new(); - io::stdin() - .read_line(&mut project_name) - .expect("Generic error message that doesn't help you at all 🤣"); - - let project_name = project_name.trim(); - - // Generate project - let output = Command::new("bun") - .arg("create") - .arg("elysia") - .arg(&project_name) - .output() - .expect( - "Failed to execute bun create command 😭. Make sure you have bun installed globally šŸ‘€" - ); - - if output.status.success() { - println!("Project '{}' created successfully 🄳", project_name); - // Add dependencies - let deps = [ - "@bogeychan/elysia-logger", - "@elysiajs/cors", - "@elysiajs/swagger", - "@sentry/bun", - "@sentry/cli", - "@types/luxon", - "jsonwebtoken", - "luxon", - "mongoose", - "winston", - "winston-daily-rotate-file", - ]; - - for dep in deps.iter() { - let _ = Command::new("bun") - .arg("add") - .arg(dep) - .current_dir(project_name) - .output() - .expect("Failed to add dependency"); - println!("Added dependency: {}", dep); - } - - // Display each command in the terminal for debugging purposes - let command = format!("xcopy src\\templates\\src {}\\src /s /e /y", project_name); - println!("Running command: {}", command); - let _ = Command::new("cmd") - .args(&["/C", &command]) - .output() - .expect("Failed to copy template"); - } else { - let error_message = String::from_utf8_lossy(&output.stderr); - println!("Failed to create project, should I be a designer instead? šŸ¤”šŸ„ŗ: {}", error_message); + if let Err(e) = Cli::run() { + Cli::display_error(&e); + process::exit(1); } } From a6b09472971785561e3c8958af3fc8e0997ad6c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:50:45 +0000 Subject: [PATCH 3/4] Fix code review issues: safer metadata handling and remove test panic Co-authored-by: KevinArce <83199462+KevinArce@users.noreply.github.com> --- src/generator.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/generator.rs b/src/generator.rs index 64aa83f..42dc9ad 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -163,11 +163,17 @@ impl ProjectGenerator { Self::copy_dir_recursive(&src_path, &dst_path)?; } else { // Only copy if destination doesn't exist or is different - let should_copy = !dst_path.exists() || { - let src_metadata = std::fs::metadata(&src_path)?; - let dst_metadata = std::fs::metadata(&dst_path)?; - src_metadata.len() != dst_metadata.len() - || src_metadata.modified()? > dst_metadata.modified()? + let should_copy = if !dst_path.exists() { + true + } else { + // Both src and dst exist, compare metadata + match (std::fs::metadata(&src_path), std::fs::metadata(&dst_path)) { + (Ok(src_metadata), Ok(dst_metadata)) => { + src_metadata.len() != dst_metadata.len() + || src_metadata.modified()? > dst_metadata.modified()? + } + _ => true, // If we can't read metadata, copy to be safe + } }; if should_copy { @@ -221,11 +227,11 @@ mod tests { let result = generator.validate_project_name(); assert!(result.is_err()); - if let Err(BunCliError::InvalidProjectName(_)) = result { - // Expected error type - } else { - panic!("Expected InvalidProjectName error"); - } + assert!( + matches!(result, Err(BunCliError::InvalidProjectName(_))), + "Expected InvalidProjectName error, got: {:?}", + result + ); } #[test] From 2d75cfa479969c33cc87bef77e30fcbe414ad0dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:55:31 +0000 Subject: [PATCH 4/4] Apply clippy pedantic improvements: inline format args and remove unused self Co-authored-by: KevinArce <83199462+KevinArce@users.noreply.github.com> --- src/cli.rs | 6 +++--- src/generator.rs | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4d8726c..50ce078 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,7 +22,7 @@ impl Cli { generator.generate()?; println!("\n🄳 All done! Your project is ready to use."); - println!("Run 'cd {}' to get started!", project_name); + println!("Run 'cd {project_name}' to get started!"); Ok(()) } @@ -36,12 +36,12 @@ impl Cli { /// Display an error message pub fn display_error(error: &dyn std::error::Error) { - eprintln!("\nāŒ Error: {}", error); + eprintln!("\nāŒ Error: {error}"); // Display the chain of errors if available let mut source = error.source(); while let Some(err) = source { - eprintln!(" Caused by: {}", err); + eprintln!(" Caused by: {err}"); source = err.source(); } } diff --git a/src/generator.rs b/src/generator.rs index 42dc9ad..e92b2e3 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -53,7 +53,7 @@ impl ProjectGenerator { // Check for invalid characters (basic validation) if name.contains(['/', '\\', '\0']) { return Err(BunCliError::InvalidProjectName( - format!("Project name '{}' contains invalid characters", name), + format!("Project name '{name}' contains invalid characters"), )); } @@ -61,7 +61,7 @@ impl ProjectGenerator { } /// Check if Bun is installed on the system - fn check_bun_installed(&self) -> Result<()> { + fn check_bun_installed() -> Result<()> { let output = Command::new("bun") .arg("--version") .output() @@ -117,8 +117,8 @@ impl ProjectGenerator { for dep in &self.config.dependencies { // Attempt to install, but continue if one fails match self.install_dependency(dep) { - Ok(_) => println!("āœ“ Added dependency: {}", dep), - Err(e) => eprintln!("⚠ Warning: {}", e), + Ok(()) => println!("āœ“ Added dependency: {dep}"), + Err(e) => eprintln!("⚠ Warning: {e}"), } } Ok(()) @@ -191,12 +191,13 @@ impl ProjectGenerator { self.validate_project_name()?; // Check if Bun is installed - self.check_bun_installed()?; + Self::check_bun_installed()?; // Create base project - println!("Creating project '{}'...", self.config.name); + let project_name = &self.config.name; + println!("Creating project '{project_name}'..."); self.create_base_project()?; - println!("āœ“ Project '{}' created successfully", self.config.name); + println!("āœ“ Project '{project_name}' created successfully"); // Install dependencies println!("Installing dependencies..."); @@ -205,8 +206,8 @@ impl ProjectGenerator { // Copy templates println!("Copying template files..."); match self.copy_templates() { - Ok(_) => println!("āœ“ Template files copied successfully"), - Err(e) => eprintln!("⚠ Warning: {}", e), + Ok(()) => println!("āœ“ Template files copied successfully"), + Err(e) => eprintln!("⚠ Warning: {e}"), } Ok(())