mecha10-cli 0.8.0

Mecha10 CLI tool
Documentation
//! Dev command handler
//!
//! Orchestrates development mode with hot reload using the `DevSession` service.

pub mod command_listener;
pub mod ops;
pub mod session;

use crate::commands::dev::DevArgs;
use crate::config_validation::{validate_configs, ConfigKind};
use crate::context::CliContext;
use anyhow::{Context, Result};
use std::path::Path;

use session::DevSession;

/// Handle dev command
///
/// Starts development mode with automatic rebuilding and hot reload. This handler is
/// ultra-thin: it simply creates a `DevSession` and executes it.
pub async fn handle_dev(ctx: &mut CliContext, args: &DevArgs) -> Result<()> {
    let kinds =
        dev_config_kinds(&ctx.working_dir).context("failed to determine config validation scope for `mecha10 dev`")?;
    let assets_dir = if crate::config_validation::needs_environment_schema(&ctx.working_dir) {
        Some(
            ctx.assets_download()
                .fetch_and_extract()
                .await
                .context("Failed to download mecha10-assets for config validation")?,
        )
    } else {
        None
    };
    let schemas_dir = assets_dir
        .as_ref()
        .map(|dir| dir.path().join("configs/simulation/schemas"));
    validate_configs(&ctx.working_dir, &kinds, schemas_dir.as_deref())
        .context("pre-flight config validation failed for `mecha10 dev`")?;

    let session = DevSession::initialize(ctx, args).await?;

    session.execute(ctx).await?;

    Ok(())
}

/// Which [`ConfigKind`]s `mecha10 dev` should validate for the project rooted at
/// `project_root`: always [`ConfigKind::ProjectAndNodes`] and
/// [`ConfigKind::SimulationEnvironments`], plus [`ConfigKind::ActiveBehaviorTree`] if
/// `mecha10.json`'s `behaviors.active` is set.
fn dev_config_kinds(project_root: &Path) -> Result<Vec<ConfigKind>> {
    let mut kinds = vec![ConfigKind::ProjectAndNodes, ConfigKind::SimulationEnvironments];

    let project_config_path = project_root.join(mecha10_cli_core::paths::PROJECT_CONFIG);
    if project_config_path.exists() {
        let content = std::fs::read_to_string(&project_config_path)
            .with_context(|| format!("failed to read {}", project_config_path.display()))?;
        let value: serde_json::Value = serde_json::from_str(&content)
            .with_context(|| format!("failed to parse JSON from {}", project_config_path.display()))?;

        let has_active_behavior = value
            .get("behaviors")
            .and_then(|b| b.get("active"))
            .and_then(|a| a.as_str())
            .is_some();
        if has_active_behavior {
            kinds.push(ConfigKind::ActiveBehaviorTree);
        }
    }

    Ok(kinds)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn dev_config_kinds_is_project_and_nodes_plus_simulation_environments_for_a_minimal_project() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("mecha10.json"),
            serde_json::json!({"name": "proj", "version": "1.0.0"}).to_string(),
        )
        .unwrap();

        assert_eq!(
            dev_config_kinds(dir.path()).unwrap(),
            vec![ConfigKind::ProjectAndNodes, ConfigKind::SimulationEnvironments]
        );
    }

    #[test]
    fn dev_config_kinds_is_project_and_nodes_plus_simulation_environments_when_no_mecha10_json_exists() {
        let dir = tempdir().unwrap();
        assert_eq!(
            dev_config_kinds(dir.path()).unwrap(),
            vec![ConfigKind::ProjectAndNodes, ConfigKind::SimulationEnvironments]
        );
    }

    #[test]
    fn dev_config_kinds_adds_active_behavior_tree_when_behaviors_active_set() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("mecha10.json"),
            serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "patrol"}}).to_string(),
        )
        .unwrap();

        assert_eq!(
            dev_config_kinds(dir.path()).unwrap(),
            vec![
                ConfigKind::ProjectAndNodes,
                ConfigKind::SimulationEnvironments,
                ConfigKind::ActiveBehaviorTree
            ]
        );
    }
}