mecha10-cli 0.7.0

Mecha10 CLI tool
Documentation
//! Dev command handler
//!
//! Orchestrates development mode with hot reload using DevSession service.
//! This handler follows the 3-layer architecture with session-based lifecycle management:
//! - Commands: Pure data structures (DevArgs)
//! - Handlers: Minimal orchestration (this file)
//! - Services: Session and business logic (DevSession, DevService, etc.)

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.
///
/// # Architecture
///
/// This handler uses a session-based approach:
/// 1. DevSession - Manages entire dev lifecycle
/// 2. DevService - Core business logic
/// 3. Services - Infrastructure (Docker, Redis, Process, etc.)
/// 4. UI components - Display functions
///
/// The handler is now ultra-thin - it simply creates a DevSession
/// and executes it. All complexity is encapsulated in the session service.
///
/// # Arguments
///
/// * `ctx` - CLI execution context
/// * `args` - Dev command arguments
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`")?;
    validate_configs(&ctx.working_dir, &kinds).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`, derived from `mecha10.json` rather than a dedicated new flag:
///
/// * [`ConfigKind::ProjectAndNodes`] always - `mecha10.json` plus every node config it
///   references is the minimum any dev session depends on.
/// * [`ConfigKind::SimulationEnvironments`] always - `validate_environment_configs` is
///   already a no-op when `configs/simulation/environments/` doesn't exist, so there's
///   nothing to gate here.
/// * [`ConfigKind::ActiveBehaviorTree`], only if `mecha10.json`'s `behaviors.active` is set -
///   mirrors `config_validation::validate_active_behavior_tree_config`'s own
///   `behaviors.active` lookup via raw `serde_json::Value` rather than a typed project-config
///   field, since that's the only place this convention is defined today.
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
            ]
        );
    }
}