xbp 0.4.0

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! Deployment configuration management module
//!
//! This module handles reading and writing deployment configurations,
//! including xbp.json files and CLI argument parsing.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::fs;
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XbpConfig {
    pub project_name: String,
    pub port: u16,
    pub build_dir: String,
    pub app_type: Option<String>,
    pub build_command: Option<String>,
    pub start_command: Option<String>,
    pub install_command: Option<String>,
    pub environment: Option<HashMap<String, String>>,
}

#[derive(Debug, Clone)]
pub struct DeploymentConfig {
    pub app_name: String,
    pub port: u16,
    pub app_dir: PathBuf,
    pub build_command: Option<String>,
    pub start_command: Option<String>,
    pub install_command: Option<String>,
    pub environment: HashMap<String, String>,
}

impl DeploymentConfig {
    /// Create deployment config from CLI arguments and xbp.json fallback
    pub async fn from_args_or_config(
        app_name: Option<String>,
        port: Option<u16>,
        app_dir: Option<PathBuf>,
        config_path: Option<PathBuf>,
    ) -> Result<Self, String> {
        // Try to load from xbp.json if not all args provided
        let xbp_config = if app_name.is_none() || port.is_none() || app_dir.is_none() {
            match Self::load_xbp_config(config_path).await {
                Ok(config) => Some(config),
                Err(_) => None, // Ignore config file errors if we have CLI args
            }
        } else {
            None
        };

        let app_name = app_name
            .or_else(|| xbp_config.as_ref().map(|c| c.project_name.clone()))
            .ok_or("Missing app name")?;

        let port = port
            .or_else(|| xbp_config.as_ref().map(|c| c.port))
            .ok_or("Missing port")?;

        let app_dir = app_dir
            .or_else(|| xbp_config.as_ref().map(|c| PathBuf::from(&c.build_dir)))
            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

        let app_dir = app_dir.canonicalize()
            .map_err(|e| format!("Failed to resolve app directory: {}", e))?;

        // Get build/start commands from config if available
        let build_command = xbp_config.as_ref().and_then(|c| c.build_command.clone());
        let start_command = xbp_config.as_ref().and_then(|c| c.start_command.clone());
        let install_command = xbp_config.as_ref().and_then(|c| c.install_command.clone());
        let environment = xbp_config.as_ref()
            .and_then(|c| c.environment.clone())
            .unwrap_or_default();

        Ok(DeploymentConfig {
            app_name,
            port,
            app_dir,
            build_command,
            start_command,
            install_command,
            environment,
        })
    }

    /// Load xbp.json configuration from file
    async fn load_xbp_config(config_path: Option<PathBuf>) -> Result<XbpConfig, String> {
        let config_path = config_path.unwrap_or_else(|| {
            std::env::current_dir()
                .unwrap_or_default()
                .join(".xbp")
                .join("xbp.json")
        });

        if !config_path.exists() {
            return Err(format!("Configuration file not found: {}", config_path.display()));
        }

        let content = fs::read_to_string(&config_path)
            .map_err(|e| format!("Failed to read config file: {}", e))?;

        let config: XbpConfig = serde_json::from_str(&content)
            .map_err(|e| format!("Failed to parse config file: {}", e))?;

        Ok(config)
    }

    /// Save updated configuration back to xbp.json
    pub async fn save_xbp_config(&self, config_path: Option<PathBuf>) -> Result<(), String> {
        let config_path = config_path.unwrap_or_else(|| {
            self.app_dir.join(".xbp").join("xbp.json")
        });

        // Create .xbp directory if it doesn't exist
        if let Some(parent) = config_path.parent() {
            fs::create_dir_all(parent)
                .map_err(|e| format!("Failed to create config directory: {}", e))?;
        }

        let xbp_config = XbpConfig {
            project_name: self.app_name.clone(),
            port: self.port,
            build_dir: self.app_dir.to_string_lossy().to_string(),
            app_type: None, // Will be detected automatically
            build_command: self.build_command.clone(),
            start_command: self.start_command.clone(),
            install_command: self.install_command.clone(),
            environment: if self.environment.is_empty() {
                None
            } else {
                Some(self.environment.clone())
            },
        };

        let content = serde_json::to_string_pretty(&xbp_config)
            .map_err(|e| format!("Failed to serialize config: {}", e))?;

        fs::write(&config_path, content)
            .map_err(|e| format!("Failed to write config file: {}", e))?;

        Ok(())
    }

    /// Update port in the configuration
    pub fn update_port(&mut self, new_port: u16) {
        self.port = new_port;
    }

    /// Merge with deployment recommendations from project detection
    pub fn merge_with_recommendations(
        &mut self,
        recommendations: &super::project_detector::DeploymentRecommendations,
    ) {
        // Use recommendations if not already set
        if self.build_command.is_none() {
            self.build_command = recommendations.build_command.clone();
        }
        
        if self.start_command.is_none() {
            self.start_command = recommendations.start_command.clone();
        }
        
        if self.install_command.is_none() {
            self.install_command = recommendations.install_command.clone();
        }

        // Use recommended process name if app_name is generic
        if let Some(recommended_name) = &recommendations.process_name {
            if self.app_name == "app" || self.app_name == "unknown" {
                self.app_name = recommended_name.clone();
            }
        }
    }
}