arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
use std::fmt;

use super::ProjectError;

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ProjectName(String);

impl ProjectName {
    pub(crate) fn parse(value: &str) -> Result<Self, ProjectError> {
        let valid = !value.is_empty()
            && value.len() <= 64
            && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
            && value
                .bytes()
                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
            && !value.ends_with('-')
            && !value.contains("--");
        if valid {
            Ok(Self(value.to_owned()))
        } else {
            Err(ProjectError::InvalidName(value.to_owned()))
        }
    }
}

impl fmt::Display for ProjectName {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.0)
    }
}

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

    #[test]
    fn validates_cross_ecosystem_project_names() {
        assert!(ProjectName::parse("my-app2").is_ok());
        for invalid in [
            "",
            "MyApp",
            "my_app",
            "../app",
            "-app",
            "app-",
            "app--name",
            "đemo",
        ] {
            assert!(
                ProjectName::parse(invalid).is_err(),
                "{invalid} should be rejected"
            );
        }
    }
}