use std::io;
use std::path::Path;
use std::process::{Command, Stdio};
pub const BEVY_CLI_REPO: &str = "https://github.com/TheBevyFlock/bevy_cli";
pub const BEVY_CLI_TAG: &str = "cli-v0.1.0-alpha.2";
pub const GAME_TEMPLATE_URL: &str = "https://github.com/jbuehler23/bevy_map_editor_template";
#[derive(Debug)]
pub enum BevyCliError {
IoError(io::Error),
CargoNotFound,
InstallFailed(String),
CreateFailed(String),
InvalidProjectName(String),
}
impl std::fmt::Display for BevyCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BevyCliError::IoError(e) => write!(f, "IO error: {}", e),
BevyCliError::CargoNotFound => {
write!(f, "Cargo not found. Please install Rust toolchain.")
}
BevyCliError::InstallFailed(msg) => write!(f, "Bevy CLI installation failed: {}", msg),
BevyCliError::CreateFailed(msg) => write!(f, "Project creation failed: {}", msg),
BevyCliError::InvalidProjectName(name) => {
write!(f, "Invalid project name: '{}'. Use lowercase letters, numbers, underscores, or hyphens.", name)
}
}
}
}
impl std::error::Error for BevyCliError {}
impl From<io::Error> for BevyCliError {
fn from(e: io::Error) -> Self {
BevyCliError::IoError(e)
}
}
pub fn is_bevy_cli_installed() -> bool {
Command::new("bevy")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn is_cargo_available() -> bool {
Command::new("cargo")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn install_bevy_cli() -> Result<(), BevyCliError> {
if !is_cargo_available() {
return Err(BevyCliError::CargoNotFound);
}
let output = Command::new("cargo")
.args([
"install",
"--git",
BEVY_CLI_REPO,
"--tag",
BEVY_CLI_TAG,
"--locked",
"bevy_cli",
])
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(BevyCliError::InstallFailed(stderr.to_string()))
}
}
pub fn create_project(name: &str, parent_dir: &Path) -> Result<(), BevyCliError> {
if !is_valid_crate_name(name) {
return Err(BevyCliError::InvalidProjectName(name.to_string()));
}
let output = Command::new("bevy")
.args([
"new",
name,
"--template",
GAME_TEMPLATE_URL,
"--",
"--define",
"author=",
"--define",
"description=A game created with bevy_map_editor",
])
.current_dir(parent_dir)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(BevyCliError::CreateFailed(stderr.to_string()))
}
}
fn is_valid_crate_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first = name.chars().next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return false;
}
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
}
pub fn get_bevy_cli_version() -> Option<String> {
let output = Command::new("bevy").arg("--version").output().ok()?;
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout);
Some(version.trim().to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_crate_names() {
assert!(is_valid_crate_name("my_game"));
assert!(is_valid_crate_name("my-game"));
assert!(is_valid_crate_name("game123"));
assert!(is_valid_crate_name("_private"));
}
#[test]
fn test_invalid_crate_names() {
assert!(!is_valid_crate_name(""));
assert!(!is_valid_crate_name("123game")); assert!(!is_valid_crate_name("My_Game")); assert!(!is_valid_crate_name("my game")); }
}