arcature-cli 2026.0.0

Developer lifecycle CLI for Arcature applications.
Documentation
use std::fs;
use std::path::{Component, Path, PathBuf};

use serde::Deserialize;

use super::{Frontend, ProjectError};

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ProjectConfig {
    #[serde(skip)]
    root: PathBuf,
    pub(crate) frontend: Frontend,
    pub(crate) frontend_dir: PathBuf,
    pub(crate) backend_package: String,
    pub(crate) backend_binary: String,
    pub(crate) backend_port: u16,
    pub(crate) frontend_port: u16,
}

impl ProjectConfig {
    pub(crate) fn load(root: &Path) -> Result<Self, ProjectError> {
        let path = root.join("arcature.toml");
        let source = fs::read_to_string(&path).map_err(|source| ProjectError::ReadConfig {
            path: path.clone(),
            source,
        })?;
        let mut config: Self = toml::from_str(&source)
            .map_err(|source| ProjectError::InvalidConfig { path, source })?;
        config.validate()?;
        config.root = root.to_path_buf();
        Ok(config)
    }

    pub(crate) fn root(&self) -> &Path {
        &self.root
    }
    pub(crate) fn frontend_root(&self) -> PathBuf {
        self.root.join(&self.frontend_dir)
    }

    fn validate(&self) -> Result<(), ProjectError> {
        if self.frontend_dir.as_os_str().is_empty()
            || self.frontend_dir.is_absolute()
            || self
                .frontend_dir
                .components()
                .any(|part| !matches!(part, Component::Normal(_)))
        {
            return Err(ProjectError::InvalidField {
                field: "frontend_dir",
                value: self.frontend_dir.display().to_string(),
            });
        }
        super::ProjectName::parse(&self.backend_package).map_err(|_| {
            ProjectError::InvalidField {
                field: "backend_package",
                value: self.backend_package.clone(),
            }
        })?;
        super::ProjectName::parse(&self.backend_binary).map_err(|_| {
            ProjectError::InvalidField {
                field: "backend_binary",
                value: self.backend_binary.clone(),
            }
        })?;
        if self.backend_port == 0
            || self.frontend_port == 0
            || self.backend_port == self.frontend_port
        {
            return Err(ProjectError::InvalidField {
                field: "ports",
                value: format!("{}/{}", self.backend_port, self.frontend_port),
            });
        }
        Ok(())
    }
}